// Expect retcode = 0 // 路径工具(不访问文件系统):同时接受 '/' 与 '\\';支持 Windows 盘符 "C:"。 // 输入为借用 ByteView,输出写入调用方的 ByteBuffer。 // 注意:normalize 对相对路径的前导 ".." 采取丢弃(不保留),适用于导入路径场景。 type .{ .Error, .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 == '\\'; // '/' '\\' } fn is_drive(path: ByteView): bool { ret path.len >= 2 && is_alpha(path.ptr[0]) && path.ptr[1] == ':'; // 盘符首字符必须是字母 } fn is_absolute(path: ByteView): bool { if path.len == 0 { ret false; } if is_sep(path.ptr[0]) { ret true; } ret is_drive(path); } // 最后一个分隔符下标;无返回 ~0。 fn last_sep(path: ByteView): usize { var i = path.len; while i > 0 { i = i - 1; if is_sep(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(); }, // 防御:索引已界内 } } var r = ByteView.slice(path, 0, idx); match r { .Ok(v) => { ret v; }, .Err(error) => { ret ByteView.empty(); }, // 防御:索引已界内 } ret ByteView.empty(); } 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 } ret ByteView.empty(); } // 扩展名(不含 '.');无扩展名返回空。 fn extension(path: ByteView): ByteView { var base = basename(path); var i = base.len; while i > 0 { i = i - 1; 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 ByteView.empty(); } // out = dir + sep + rel(rel 绝对时直接使用 rel);成功后才替换 out,失败保持 out 不变。 fn join_into(out: *ByteBuffer, dir: ByteView, rel: ByteView): Status { var fresh = ByteBuffer.new(); if is_absolute(rel) { match ByteBuffer.append(&fresh, ByteView.as_slice(rel)) { .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 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(rel)) { .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; } var starts = Vec.blank(@sizeof(usize), 8); var i: 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); }, } } 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; } if i >= path.len { break; } var start = i; while i < path.len && !is_sep(path.ptr[i]) { i = i + 1; } 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; } 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); }, } } 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); }, } } 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 view(text: []u8): ByteView { ret ByteView.from_slice(text); } fn expect_view(got: ByteView, expected: []u8) { @assert(ByteView.eq(got, view(expected)), "view value"); } fn expect_out(out: *ByteBuffer, expected: []u8) { var actual = ByteBuffer.as_slice(out.*); @assert(actual.len == expected.len, "out length"); var i: usize = 0; while i < expected.len { @assert(actual.ptr[i] == expected.ptr[i], "out 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_join() { var out = ByteBuffer.new(); defer { ByteBuffer.drop(&out); } expect_ok(join_into(&out, view("stage2/utils"), view("vec.spl"))); 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"); } fn test_normalize() { var out = ByteBuffer.new(); defer { ByteBuffer.drop(&out); } expect_ok(normalize_into(&out, view("a/b/../c"))); 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, ""); } fn main() { test_parts(); test_join(); test_normalize(); }