Files
spl/stage2/utils/path.spl
T
2026-09-17 22:32:36 +08:00

232 lines
7.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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");
fn is_sep(c: u8): bool {
ret c == '/' || c == '\\'; // '/' '\\'
}
fn is_drive(path: ByteView): bool {
ret path.len >= 2 && 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);
}
// 目录部分(不含末尾分隔符);根 "/" 返回 "/";无分隔符返回空。
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(); },
}
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(); },
}
}
}
ret ByteView.empty();
}
// out = dir + sep + rel(rel 绝对时直接使用 rel);先清空 out。
fn join_into(out: *ByteBuffer, dir: ByteView, rel: ByteView): Status {
ByteBuffer.clear(out);
if is_absolute(rel) {
ret ByteBuffer.append(out, ByteView.as_slice(rel));
}
match ByteBuffer.append(out, ByteView.as_slice(dir)) { .Ok => {}, .Err(e) => { ret .Err(e); } }
if dir.len != 0 && !is_sep(dir.ptr[dir.len - 1]) {
match ByteBuffer.append_byte(out, '/') { .Ok => {}, .Err(e) => { ret .Err(e); } }
}
ret ByteBuffer.append(out, ByteView.as_slice(rel));
}
// 折叠重复分隔符、'.',及'..'(回退上一段;相对路径前导 '..' 丢弃)。
fn normalize_into(out: *ByteBuffer, path: ByteView): Status {
ByteBuffer.clear(out);
if path.len == 0 { ret .Ok; }
var starts = Vec.blank(@sizeof(usize), 8);
var i: usize = 0;
if is_sep(path.ptr[0]) {
match ByteBuffer.append_byte(out, '/') { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
} else if is_drive(path) {
match ByteBuffer.append_byte(out, path.ptr[0]) { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
match ByteBuffer.append_byte(out, ':') { .Ok => {}, .Err(e) => { Vec.drop(&starts); 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); 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) => { out.storage.len = (p as *usize).*; },
.Err(error) => {},
}
starts.len = starts.len - 1;
}
continue;
}
var comp_start = out.storage.len;
if comp_start > 0 && !is_sep(out.storage.data[comp_start - 1]) {
match ByteBuffer.append_byte(out, '/') { .Ok => {}, .Err(e) => { Vec.drop(&starts); ret .Err(e); } }
}
var recorded = comp_start;
match Vec.push(&starts, &recorded as *u8) {
.Ok => {},
.Err(error) => { Vec.drop(&starts); ret .Err(error); },
}
match ByteBuffer.append(out, ByteView.as_slice(seg)) {
.Ok => {},
.Err(error) => { Vec.drop(&starts); ret .Err(error); },
}
}
Vec.drop(&starts);
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();
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");
ByteBuffer.drop(&out);
}
fn test_normalize() {
var out = ByteBuffer.new();
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, "");
ByteBuffer.drop(&out);
}
fn main() {
test_parts();
test_join();
test_normalize();
}