stage2 更多的工具库

This commit is contained in:
zzy committed 2026-09-18 11:17:33 +08:00
1 parent b3e38ba7e4
commit c30143609a
8 files changed
+2711 -1138

No files matched your search

+289 -156
View File
@@ -1,267 +1,400 @@
// Expect retcode = 0
// 路径工具(不访问文件系统):同时接受 '/' 与 '\\';支持 Windows 盘符 "C:"。
// 输入为借用 ByteView,输出写入调用方的 ByteBuffer。
// 注意:normalize 对相对路径的前导 ".." 采取丢弃(不保留),适用于导入路径场景。
type .{ .Error, .Status, .make } = @import("error.spl");
// 纯词法路径工具,不访问文件系统。调用方必须显式选择 POSIX 或 Windows 规则;
// 输入视图均为借用,拥有型结果写入 ByteBuffer,失败保持原输出不变。
type .{ .Status, .make } = @import("error.spl");
type .{ .ByteView } = @import("byte_view.spl");
type .{ .ByteBuffer } = @import("byte_buffer.spl");
type .{ .Vec } = @import("vec.spl");
type .{ .is_alpha } = @import("ascii.spl");
fn is_sep(c: u8): bool {
ret c == '/' || c == '\\'; // '/' '\\'
type PathStyle = enum { Posix, Windows }
fn is_windows(style: PathStyle): bool {
match style { .Posix => { ret false; }, .Windows => { ret true; }, }
ret false;
}
fn is_drive(path: ByteView): bool {
ret path.len >= 2 && is_alpha(path.ptr[0]) && path.ptr[1] == ':'; // 盘符首字符必须是字母
fn separator(style: PathStyle): u8 {
if is_windows(style) { ret '\\'; }
ret '/';
}
fn is_absolute(path: ByteView): bool {
fn is_sep(style: PathStyle, byte: u8): bool {
if is_windows(style) { ret byte == '/' || byte == '\\'; }
ret byte == '/';
}
fn is_drive_prefix(path: ByteView): bool {
ret path.len >= 2 && is_alpha(path.ptr[0]) && path.ptr[1] == ':';
}
fn is_unc(path: ByteView, style: PathStyle): bool {
ret is_windows(style) && path.len >= 2 &&
is_sep(style, path.ptr[0]) && is_sep(style, path.ptr[1]);
}
fn is_absolute(path: ByteView, style: PathStyle): bool {
if path.len == 0 { ret false; }
if is_sep(path.ptr[0]) { ret true; }
ret is_drive(path);
if !is_windows(style) { ret path.ptr[0] == '/'; }
if is_unc(path, style) { ret true; }
if is_drive_prefix(path) {
ret path.len >= 3 && is_sep(style, path.ptr[2]);
}
// 根相对路径以当前盘符为根,本模块将其视为绝对。
ret is_sep(style, path.ptr[0]);
}
// 最后一个分隔符下标;无返回 ~0。
fn last_sep(path: ByteView): usize {
// UNC 根长度不含共享名后的分隔符;不完整时返回 0。
fn unc_root_len(path: ByteView, style: PathStyle): usize {
if !is_unc(path, style) { ret 0; }
var i: usize = 2;
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
var server = i;
while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; }
if i == server { ret 0; }
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
var share = i;
while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; }
if i == share { ret 0; }
ret i;
}
fn root_len(path: ByteView, style: PathStyle): usize {
if path.len == 0 { ret 0; }
if !is_windows(style) {
if path.ptr[0] == '/' { ret 1; }
ret 0;
}
var unc = unc_root_len(path, style);
if unc != 0 { ret unc; }
if is_drive_prefix(path) {
if path.len >= 3 && is_sep(style, path.ptr[2]) { ret 3; }
ret 2;
}
if is_sep(style, path.ptr[0]) { ret 1; }
ret 0;
}
fn last_sep(path: ByteView, style: PathStyle): usize {
var i = path.len;
while i > 0 {
i = i - 1;
if is_sep(path.ptr[i]) { ret i; }
if is_sep(style, path.ptr[i]) { ret i; }
}
ret ~(0 as usize);
}
// 目录部分(不含末尾分隔符);根 "/" 返回 "/";无分隔符返回空。
// 下标均由 last_sep 保证在界内,slice 不会失败;Err 分支仅作防御。
fn dirname(path: ByteView): ByteView {
var idx = last_sep(path);
if idx == ~(0 as usize) { ret ByteView.empty(); }
if idx == 0 {
var r = ByteView.slice(path, 0, 1);
match r {
.Ok(v) => { ret v; },
.Err(error) => { ret ByteView.empty(); }, // 防御:索引已界内
fn slice_or_empty(path: ByteView, start: usize, end: usize): ByteView {
match ByteView.slice(path, start, end) {
.Ok(value) => { ret value; },
.Err(error) => { ret ByteView.empty(); },
}
ret ByteView.empty();
}
fn dirname(path: ByteView, style: PathStyle): ByteView {
var root = root_len(path, style);
if root != 0 && path.len == root { ret path; }
var index = last_sep(path, style);
if index == ~(0 as usize) {
if is_windows(style) && is_drive_prefix(path) {
ret slice_or_empty(path, 0, 2);
}
ret ByteView.empty();
}
var r = ByteView.slice(path, 0, idx);
match r {
.Ok(v) => { ret v; },
.Err(error) => { ret ByteView.empty(); }, // 防御:索引已界内
}
ret ByteView.empty();
if root != 0 && index <= root { ret slice_or_empty(path, 0, root); }
if index == 0 { ret slice_or_empty(path, 0, 1); }
ret slice_or_empty(path, 0, index);
}
fn basename(path: ByteView): ByteView {
var idx = last_sep(path);
if idx == ~(0 as usize) { ret path; }
var r = ByteView.slice(path, idx + 1, path.len);
match r {
.Ok(v) => { ret v; },
.Err(error) => { ret ByteView.empty(); }, // 防御:idx+1 <= len
fn basename(path: ByteView, style: PathStyle): ByteView {
var root = root_len(path, style);
if root != 0 && path.len == root { ret ByteView.empty(); }
var index = last_sep(path, style);
if index == ~(0 as usize) {
if is_windows(style) && is_drive_prefix(path) {
ret slice_or_empty(path, 2, path.len);
}
ret path;
}
ret ByteView.empty();
ret slice_or_empty(path, index + 1, path.len);
}
// 扩展名(不含 '.');无扩展名返回空。
fn extension(path: ByteView): ByteView {
var base = basename(path);
fn extension(path: ByteView, style: PathStyle): ByteView {
var base = basename(path, style);
var i = base.len;
while i > 0 {
i = i - 1;
if base.ptr[i] == '.' { // '.'
if base.ptr[i] == '.' {
if i == 0 { ret ByteView.empty(); }
var r = ByteView.slice(base, i + 1, base.len);
match r {
.Ok(v) => { ret v; },
.Err(error) => { ret ByteView.empty(); }, // 防御:i+1 <= base.len
}
ret slice_or_empty(base, i + 1, base.len);
}
}
ret ByteView.empty();
}
// out = dir + sep + rel(rel 绝对时直接使用 rel);成功后才替换 out,失败保持 out 不变。
fn join_into(out: *ByteBuffer, dir: ByteView, rel: ByteView): Status {
// Windows 盘符相对路径依赖进程的盘符工作目录,join 明确拒绝该歧义。
fn join_into(out: *ByteBuffer, dir: ByteView,
rel: ByteView, style: PathStyle): Status {
if out == null { ret .Err(make(.InvalidArgument)); }
var fresh = ByteBuffer.new();
if is_absolute(rel) {
if is_absolute(rel, style) {
match ByteBuffer.append(&fresh, ByteView.as_slice(rel)) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
}
ByteBuffer.drop(out);
out.* = ByteBuffer.take(&fresh);
ret .Ok;
}
match ByteBuffer.append(&fresh, ByteView.as_slice(dir)) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
if is_windows(style) && is_drive_prefix(rel) {
ByteBuffer.drop(&fresh);
ret .Err(make(.InvalidArgument));
}
if dir.len != 0 && !is_sep(dir.ptr[dir.len - 1]) {
match ByteBuffer.append_byte(&fresh, '/') {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
match ByteBuffer.append(&fresh, ByteView.as_slice(dir)) {
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
}
var drive_relative_base = is_windows(style) && dir.len == 2 && is_drive_prefix(dir);
if dir.len != 0 && !is_sep(style, dir.ptr[dir.len - 1]) && rel.len != 0 &&
!drive_relative_base {
match ByteBuffer.append_byte(&fresh, separator(style)) {
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
}
}
match ByteBuffer.append(&fresh, ByteView.as_slice(rel)) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
.Ok => {}, .Err(error) => { ByteBuffer.drop(&fresh); ret .Err(error); },
}
ByteBuffer.drop(out);
out.* = ByteBuffer.take(&fresh);
ret .Ok;
}
// 折叠重复分隔符、'.',及'..'(回退上一段;相对路径前导 '..' 丢弃)。
// 成功后才替换 out,失败保持 out 不变。
fn normalize_into(out: *ByteBuffer, path: ByteView): Status {
var fresh = ByteBuffer.new();
if path.len == 0 {
ByteBuffer.drop(out);
out.* = ByteBuffer.take(&fresh);
ret .Ok;
fn segment_is_parent(buffer: ByteBuffer, rollback: usize, style: PathStyle): bool {
var start = rollback;
if start < buffer.storage.len && is_sep(style, buffer.storage.data[start]) {
start = start + 1;
}
ret buffer.storage.len - start == 2 &&
buffer.storage.data[start] == '.' && buffer.storage.data[start + 1] == '.';
}
var starts = Vec.blank(@sizeof(usize), 8);
fn append_segment(out: *ByteBuffer, starts: *Vec,
segment: ByteView, style: PathStyle): Status {
var rollback = out.storage.len;
if rollback != 0 && !is_sep(style, out.storage.data[rollback - 1]) &&
!(is_windows(style) && out.storage.data[rollback - 1] == ':') {
match ByteBuffer.append_byte(out, separator(style)) {
.Ok => {}, .Err(error) => { ret .Err(error); },
}
}
match Vec.push(starts, &rollback as *u8) {
.Ok => {},
.Err(error) => { out.storage.len = rollback; ret .Err(error); },
}
match ByteBuffer.append(out, ByteView.as_slice(segment)) {
.Ok => {},
.Err(error) => {
starts.len = starts.len - 1;
out.storage.len = rollback;
ret .Err(error);
},
}
ret .Ok;
}
// 规范化分隔符并折叠 '.'/'..'。相对路径无法消去的前导 '..' 会被保留;
// 绝对路径的 '..' 不会越过根,UNC 的 server/share 也不可被消去。
fn normalize_into(out: *ByteBuffer, path: ByteView, style: PathStyle): Status {
if out == null { ret .Err(make(.InvalidArgument)); }
var fresh = ByteBuffer.new();
var starts = Vec.blank(@sizeof(usize), @alignof(usize));
var i: usize = 0;
var absolute = false;
var protected: usize = 0;
if is_sep(path.ptr[0]) {
match ByteBuffer.append_byte(&fresh, '/') {
.Ok => {}, .Err(e) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(e); },
if path.len != 0 {
if !is_windows(style) && path.ptr[0] == '/' {
absolute = true;
match ByteBuffer.append_byte(&fresh, '/') {
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
} else if is_unc(path, style) {
absolute = true;
protected = 2;
match ByteBuffer.append(&fresh, "\\\\") {
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
i = 2;
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
} else if is_windows(style) && is_drive_prefix(path) {
match ByteBuffer.append(&fresh, path.ptr[0..2]) {
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
i = 2;
if i < path.len && is_sep(style, path.ptr[i]) {
absolute = true;
match ByteBuffer.append_byte(&fresh, '\\') {
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
}
} else if is_windows(style) && is_sep(style, path.ptr[0]) {
absolute = true;
match ByteBuffer.append_byte(&fresh, '\\') {
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
}
} else if is_drive(path) {
match ByteBuffer.append_byte(&fresh, path.ptr[0]) {
.Ok => {}, .Err(e) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(e); },
}
match ByteBuffer.append_byte(&fresh, ':') {
.Ok => {}, .Err(e) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(e); },
}
i = 2; // 跳过盘符
}
while i < path.len {
while i < path.len && is_sep(path.ptr[i]) { i = i + 1; }
while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; }
if i >= path.len { break; }
var start = i;
while i < path.len && !is_sep(path.ptr[i]) { i = i + 1; }
var begin = i;
while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; }
var segment = slice_or_empty(path, begin, i);
var seg_result = ByteView.slice(path, start, i);
var seg = ByteView.empty();
match seg_result {
.Ok(v) => { seg = v; },
.Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
if ByteView.eq(seg, ByteView.from_slice(".")) { continue; }
if ByteView.eq(seg, ByteView.from_slice("..")) {
if starts.len > 0 {
var idx = starts.len - 1;
match Vec.get(&starts, idx) {
.Ok(p) => { fresh.storage.len = (p as *usize).*; },
.Err(error) => {}, // starts.len 已保证 idx 在界内,get 不会失败
}
starts.len = starts.len - 1;
if ByteView.eq(segment, ByteView.from_slice(".")) {
if protected != 0 && starts.len < protected {
Vec.drop(&starts); ByteBuffer.drop(&fresh);
ret .Err(make(.InvalidArgument));
}
continue;
}
var comp_start = fresh.storage.len;
if comp_start > 0 && !is_sep(fresh.storage.data[comp_start - 1]) {
match ByteBuffer.append_byte(&fresh, '/') {
.Ok => {}, .Err(e) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(e); },
if ByteView.eq(segment, ByteView.from_slice("..")) {
if protected != 0 && starts.len < protected {
Vec.drop(&starts); ByteBuffer.drop(&fresh);
ret .Err(make(.InvalidArgument));
}
if starts.len > protected {
var last: usize = 0;
match Vec.get(&starts, starts.len - 1) {
.Ok(ptr) => { last = (ptr as *usize).*; },
.Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
if !segment_is_parent(fresh, last, style) {
fresh.storage.len = last;
starts.len = starts.len - 1;
continue;
}
}
if absolute { continue; }
}
var recorded = comp_start;
match Vec.push(&starts, &recorded as *u8) {
.Ok => {},
.Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
match ByteBuffer.append(&fresh, ByteView.as_slice(seg)) {
.Ok => {},
.Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
match append_segment(&fresh, &starts, segment, style) {
.Ok => {}, .Err(error) => { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(error); },
}
}
if protected != 0 && starts.len < protected {
Vec.drop(&starts); ByteBuffer.drop(&fresh);
ret .Err(make(.InvalidArgument));
}
Vec.drop(&starts);
ByteBuffer.drop(out);
out.* = ByteBuffer.take(&fresh);
ret .Ok;
}
// ---- 测试辅助 ----
// ---- 测试 ----
fn expect_ok(result: Status) {
match result { .Ok => {}, .Err(error) => { @assert(false, "unexpected path error"); }, }
}
fn expect_invalid(result: Status) {
match result {
.Ok => {},
.Err(error) => { @assert(false, "unexpected path error"); },
.Ok => { @assert(false, "expected invalid path"); },
.Err(error) => {
match error.kind { .InvalidArgument => {}, _ => { @assert(false); }, }
},
}
}
fn view(text: []u8): ByteView {
ret ByteView.from_slice(text);
}
fn view(text: []u8): ByteView { ret ByteView.from_slice(text); }
fn expect_view(got: ByteView, expected: []u8) {
@assert(ByteView.eq(got, view(expected)), "view value");
@assert(ByteView.eq(got, view(expected)), "path view");
}
fn expect_out(out: *ByteBuffer, expected: []u8) {
var actual = ByteBuffer.as_slice(out.*);
@assert(actual.len == expected.len, "out length");
@assert(actual.len == expected.len, "path output length");
var i: usize = 0;
while i < expected.len {
@assert(actual.ptr[i] == expected.ptr[i], "out byte");
@assert(actual.ptr[i] == expected.ptr[i], "path output byte");
i = i + 1;
}
}
fn test_parts() {
@assert(is_sep('/') && is_sep('\\'), "seps");
@assert(is_absolute(view("/a/b")), "posix abs");
@assert(is_absolute(view("\\a")), "win abs");
@assert(is_absolute(view("C:/a")), "drive abs");
@assert(!is_absolute(view("a/b")), "relative");
expect_view(dirname(view("/a/b/c")), "/a/b");
expect_view(dirname(view("/a")), "/");
expect_view(dirname(view("abc")), "");
expect_view(basename(view("/a/b/c")), "c");
expect_view(basename(view("abc")), "abc");
expect_view(extension(view("a/b.spl")), "spl");
expect_view(extension(view("a/b")), "");
expect_view(extension(view("a/.hidden")), "");
fn test_style_and_parts() {
@assert(is_absolute(view("/a"), .Posix));
@assert(!is_absolute(view("\\a"), .Posix));
@assert(is_absolute(view("C:\\a"), .Windows));
@assert(!is_absolute(view("C:a"), .Windows));
@assert(is_absolute(view("\\\\server\\share\\a"), .Windows));
@assert(!is_absolute(view("C:/a"), .Posix));
expect_view(dirname(view("/a/b"), .Posix), "/a");
expect_view(dirname(view("/a"), .Posix), "/");
expect_view(basename(view("/a/b"), .Posix), "b");
expect_view(extension(view("a/b.spl"), .Posix), "spl");
expect_view(extension(view("a/.hidden"), .Posix), "");
expect_view(dirname(view("C:\\a"), .Windows), "C:\\");
expect_view(basename(view("C:\\a"), .Windows), "a");
expect_view(dirname(view("C:relative"), .Windows), "C:");
expect_view(basename(view("C:relative"), .Windows), "relative");
expect_view(dirname(view("\\\\server\\share\\file"), .Windows),
"\\\\server\\share");
}
fn test_join() {
var out = ByteBuffer.new();
defer { ByteBuffer.drop(&out); }
expect_ok(join_into(&out, view("stage2/utils"), view("vec.spl")));
expect_ok(join_into(&out, view("stage2/utils"), view("vec.spl"), .Posix));
expect_out(&out, "stage2/utils/vec.spl");
expect_ok(join_into(&out, view("a/"), view("b.spl")));
expect_out(&out, "a/b.spl");
expect_ok(join_into(&out, view("ignored"), view("/abs/x")));
expect_out(&out, "/abs/x");
expect_ok(join_into(&out, view("C:\\base"), view("child\\x"), .Windows));
expect_out(&out, "C:\\base\\child\\x");
expect_ok(join_into(&out, view("C:"), view("child"), .Windows));
expect_out(&out, "C:child");
expect_ok(join_into(&out, view("ignored"), view("D:/abs"), .Windows));
expect_out(&out, "D:/abs");
expect_invalid(join_into(&out, view("C:\\base"), view("D:relative"), .Windows));
expect_out(&out, "D:/abs");
}
fn test_normalize() {
fn test_normalize_posix() {
var out = ByteBuffer.new();
defer { ByteBuffer.drop(&out); }
expect_ok(normalize_into(&out, view("a/b/../c")));
expect_ok(normalize_into(&out, view("a/b/../c"), .Posix));
expect_out(&out, "a/c");
expect_ok(normalize_into(&out, view("./a//b")));
expect_out(&out, "a/b");
expect_ok(normalize_into(&out, view("/a/./b/../c")));
expect_out(&out, "/a/c");
expect_ok(normalize_into(&out, view("C:/a/../b")));
expect_out(&out, "C:/b");
expect_ok(normalize_into(&out, view("a/..")));
expect_out(&out, "");
expect_ok(normalize_into(&out, view("../../a/./b"), .Posix));
expect_out(&out, "../../a/b");
expect_ok(normalize_into(&out, view("/../../a//b"), .Posix));
expect_out(&out, "/a/b");
expect_ok(normalize_into(&out, view("a\\b/../c"), .Posix));
expect_out(&out, "c");
}
fn test_normalize_windows() {
var out = ByteBuffer.new();
defer { ByteBuffer.drop(&out); }
expect_ok(normalize_into(&out, view("C:/a\\b/../c"), .Windows));
expect_out(&out, "C:\\a\\c");
expect_ok(normalize_into(&out, view("C:foo\\..\\bar"), .Windows));
expect_out(&out, "C:bar");
expect_ok(normalize_into(&out, view("\\root\\..\\file"), .Windows));
expect_out(&out, "\\file");
expect_ok(normalize_into(&out,
view("\\\\server\\share\\a\\..\\file"), .Windows));
expect_out(&out, "\\\\server\\share\\file");
expect_ok(ByteBuffer.assign(&out, "preserved"));
expect_invalid(normalize_into(&out, view("\\\\server\\..\\file"), .Windows));
expect_out(&out, "preserved");
}
fn main() {
test_parts();
test_style_and_parts();
test_join();
test_normalize();
test_normalize_posix();
test_normalize_windows();
}