Files
spl/stage2/utils/path.spl
T

268 lines
8.7 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");
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();
}