// Expect retcode = 0 // 纯词法路径工具,不访问文件系统。调用方必须显式选择 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"); type PathStyle = enum { Posix, Windows } fn is_windows(style: PathStyle): bool { match style { .Posix => { ret false; }, .Windows => { ret true; }, } ret false; } fn separator(style: PathStyle): u8 { if is_windows(style) { ret '\\'; } ret '/'; } 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_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]); } // 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(style, path.ptr[i]) { ret i; } } ret ~(0 as usize); } 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(); } 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, 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 slice_or_empty(path, index + 1, path.len); } 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 i == 0 { ret ByteView.empty(); } ret slice_or_empty(base, i + 1, base.len); } } ret ByteView.empty(); } // 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, style) { 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; } if is_windows(style) && is_drive_prefix(rel) { ByteBuffer.drop(&fresh); ret .Err(make(.InvalidArgument)); } 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); }, } 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] == '.'; } 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 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; } } } while i < path.len { while i < path.len && is_sep(style, path.ptr[i]) { i = i + 1; } if i >= path.len { break; } var begin = i; while i < path.len && !is_sep(style, path.ptr[i]) { i = i + 1; } var segment = slice_or_empty(path, begin, i); if ByteView.eq(segment, ByteView.from_slice(".")) { if protected != 0 && starts.len < protected { Vec.drop(&starts); ByteBuffer.drop(&fresh); ret .Err(make(.InvalidArgument)); } continue; } 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; } } 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 => { @assert(false, "expected invalid path"); }, .Err(error) => { match error.kind { .InvalidArgument => {}, _ => { @assert(false); }, } }, } } fn view(text: []u8): ByteView { ret ByteView.from_slice(text); } fn expect_view(got: ByteView, expected: []u8) { @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, "path output length"); var i: usize = 0; while i < expected.len { @assert(actual.ptr[i] == expected.ptr[i], "path output byte"); i = i + 1; } } 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"), .Posix)); expect_out(&out, "stage2/utils/vec.spl"); 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_posix() { var out = ByteBuffer.new(); defer { ByteBuffer.drop(&out); } expect_ok(normalize_into(&out, view("a/b/../c"), .Posix)); expect_out(&out, "a/c"); 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_style_and_parts(); test_join(); test_normalize_posix(); test_normalize_windows(); }