stack.ha (4994B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use bytes; 5 use strings; 6 7 // Appends path elements onto the end of a [[path]] buffer. 8 // Returns the new string value of the path. 9 export fn push(p: *path, items: str...) (str | error) = { 10 for (let item .. items) { 11 let elem = strings::toutf8(item); 12 13 for (true) match (bytes::index(elem, SEP)) { 14 case void => 15 p.end = appendnorm(p, elem)?; 16 break; 17 case let j: size => 18 if (j == 0 && p.end == 0) { 19 p.buf[0] = SEP; 20 p.end = 1; 21 } else { 22 p.end = appendnorm(p, elem[..j])?; 23 }; 24 elem = elem[j+1..]; 25 }; 26 }; 27 return string(p); 28 }; 29 30 const dot: []u8 = ['.']; 31 const dotdot: []u8 = ['.', '.']; 32 33 // append a path segment to a [[path]], preserving normalization. 34 // seg must not contain any [[SEP]]s. if you need to make the path absolute, you 35 // should do that manually. returns the new end of the [[path]]. 36 // x + => x 37 // x + . => x 38 // / + .. => / 39 // + .. => .. 40 // x/.. + .. => x/../.. 41 // x/y + .. => x 42 // x + y => x/y 43 fn appendnorm(p: *path, seg: []u8) (size | error) = { 44 if (len(seg) == 0 || bytes::equal(dot, seg)) return p.end; 45 if (bytes::equal(dotdot, seg)) { 46 if (isroot(p)) return p.end; 47 const isep = match (bytes::rindex(p.buf[..p.end], SEP)) { 48 case void => yield 0z; 49 case let i: size => yield i + 1; 50 }; 51 if (p.end == 0 || bytes::equal(p.buf[isep..p.end], dotdot)) { 52 return appendlit(p, dotdot)?; 53 } else { 54 return if (isep <= 1) isep else isep - 1; 55 }; 56 } else { 57 return appendlit(p, seg)?; 58 }; 59 }; 60 61 // append a segment to a [[path]], *without* preserving normalization. 62 // returns the new end of the [[path]] 63 fn appendlit(p: *path, bs: []u8) (size | error) = { 64 let newend = p.end; 65 if (p.end == 0 || isroot(p)) { 66 if (MAX < p.end + len(bs)) return too_long; 67 } else { 68 if (MAX < p.end + len(bs) + 1) return too_long; 69 p.buf[p.end] = SEP; 70 newend += 1; 71 }; 72 p.buf[newend..newend+len(bs)] = bs; 73 return newend + len(bs); 74 }; 75 76 77 @test fn push() void = { 78 let p = init()!; 79 assert(string(&p) == "."); 80 81 // current dir invariants 82 assert(push(&p, "")! == "."); 83 assert(push(&p, ".")! == "."); 84 85 // parent dir invariants 86 assert(push(&p, "..")! == ".."); 87 assert(push(&p, "")! == ".."); 88 assert(push(&p, ".")! == ".."); 89 assert(push(&p, local("/"))! == ".."); 90 91 assert(set(&p)! == "."); 92 // root dir invariants 93 assert(push(&p, local("/"))! == local("/")); 94 assert(push(&p, "")! == local("/")); 95 assert(push(&p, ".")! == local("/")); 96 assert(push(&p, "..")! == local("/")); 97 assert(push(&p, local("/"))! == local("/")); 98 99 assert(set(&p)! == "."); 100 // regular path and parent 101 assert(push(&p, "foo")! == "foo"); 102 assert(push(&p, ".")! == "foo"); 103 assert(push(&p, local("/"))! == "foo"); 104 assert(push(&p, "..")! == "."); 105 106 // multiple segments 107 assert(push(&p, "a", "b")! == local("a/b")); 108 assert(push(&p, "..", "c")! == local("a/c")); 109 assert(push(&p, "..")! == "a"); 110 assert(push(&p, local("/d"))! == local("a/d")); 111 assert(push(&p, "..", "..")! == "."); 112 113 // multiple segments, absolute 114 assert(push(&p, local("/"), "a", "b")! == local("/a/b")); 115 assert(push(&p, "..", "c")! == local("/a/c")); 116 assert(push(&p, "..")! == local("/a")); 117 assert(push(&p, local("/d"))! == local("/a/d")); 118 assert(push(&p, "..", "..", "..")! == local("/")); 119 }; 120 121 // Examine the final path segment in a [[path]]. 122 // Returns void if the path is empty or is the root dir. 123 export fn peek(p: *const path) (str | void) = split(p).1; 124 125 // Remove and return the final path segment in a [[path]]. 126 // Returns void if the path is empty or is the root dir. 127 export fn pop(p: *path) (str | void) = { 128 const (end, res) = split(p); 129 p.end = end; 130 return res; 131 }; 132 133 // helper function for pop/peek, returns (new end of [[path]], result) 134 fn split(p: *path) (size, (str | void)) = { 135 if (p.end == 0 || isroot(p)) return (p.end, void); 136 match (bytes::rindex(p.buf[..p.end], SEP)) { 137 case void => 138 return (0z, strings::fromutf8_unsafe(p.buf[..p.end])); 139 case let i: size => 140 return ( 141 if (i == 0) 1z else i, 142 strings::fromutf8_unsafe(p.buf[i+1..p.end]), 143 ); 144 }; 145 }; 146 147 @test fn pop() void = { 148 // empty 149 let p = init()!; 150 assert(pop(&p) is void); 151 assert(string(&p) == "."); 152 153 // root dir 154 p.end = 0; 155 push(&p, local("/"))!; 156 assert(pop(&p) is void); 157 assert(string(&p) == local("/")); 158 159 // relative file 160 p.end = 0; 161 push(&p, "foo")!; 162 assert(pop(&p) as str == "foo"); 163 assert(string(&p) == "."); 164 165 // abs file 166 p.end = 0; 167 push(&p, local("/foo"))!; 168 assert(pop(&p) as str == "foo"); 169 assert(string(&p) == local("/")); 170 }; 171 172 // Returns the parent directory for a given path, without modifying the [[path]]. 173 // If the path is the root directory, the root directory is returned. The value 174 // is either borrowed from the input or statically allocated; use 175 // [[strings::dup]] to extend its lifetime or modify it. 176 export fn parent(p: *const path) (str | error) = { 177 const newend = appendnorm(p, dotdot)?; 178 if (newend == 0) return "."; 179 return strings::fromutf8_unsafe(p.buf[..newend]); 180 };