268 lines
8.7 KiB
Plaintext
268 lines
8.7 KiB
Plaintext
// 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();
|
||
}
|