File: stack.ha

package info (click to toggle)
hare 0.25.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,948 kB
  • sloc: asm: 1,264; makefile: 123; sh: 114; lisp: 101
file content (180 lines) | stat: -rw-r--r-- 5,194 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// SPDX-License-Identifier: MPL-2.0
// (c) Hare authors <https://harelang.org>

use bytes;
use strings;

// Appends path elements onto the end of a path buffer.
// Returns the new string value of the path.
export fn push(buf: *buffer, items: str...) (str | error) = {
	for (let item .. items) {
		let elem = strings::toutf8(item);

		for (true) match (bytes::index(elem, SEP)) {
		case void =>
			buf.end = appendnorm(buf, elem)?;
			break;
		case let j: size =>
			if (j == 0 && buf.end == 0) {
				buf.buf[0] = SEP;
				buf.end = 1;
			} else {
				buf.end = appendnorm(buf, elem[..j])?;
			};
			elem = elem[j+1..];
		};
	};
	return string(buf);
};

const dot: []u8 = ['.'];
const dotdot: []u8 = ['.', '.'];

// append a path segment to a buffer, preserving normalization.
// seg must not contain any [[SEP]]s. if you need to make the path absolute, you
// should do that manually. returns the new end of the buffer.
// x    +    => x
// x    + .  => x
// /    + .. => /
//      + .. => ..
// x/.. + .. => x/../..
// x/y  + .. => x
// x    + y  => x/y
fn appendnorm(buf: *buffer, seg: []u8) (size | error) = {
	if (len(seg) == 0 || bytes::equal(dot, seg)) return buf.end;
	if (bytes::equal(dotdot, seg)) {
		if (isroot(buf)) return buf.end;
		const isep = match (bytes::rindex(buf.buf[..buf.end], SEP)) {
		case void => yield 0z;
		case let i: size => yield i + 1;
		};
		if (buf.end == 0 || bytes::equal(buf.buf[isep..buf.end], dotdot)) {
			return appendlit(buf, dotdot)?;
		} else {
			return if (isep <= 1) isep else isep - 1;
		};
	} else {
		return appendlit(buf, seg)?;
	};
};

// append a segment to a buffer, *without* preserving normalization.
// returns the new end of the buffer
fn appendlit(buf: *buffer, bs: []u8) (size | error) = {
	let newend = buf.end;
	if (buf.end == 0 || isroot(buf)) {
		if (MAX < buf.end + len(bs)) return too_long;
	} else {
		if (MAX < buf.end + len(bs) + 1) return too_long;
		buf.buf[buf.end] = SEP;
		newend += 1;
	};
	buf.buf[newend..newend+len(bs)] = bs;
	return newend + len(bs);
};


@test fn push() void = {
	let buf = init()!;
	assert(string(&buf) == ".");

	// current dir invariants
	assert(push(&buf, "")! == ".");
	assert(push(&buf, ".")! == ".");

	// parent dir invariants
	assert(push(&buf, "..")! == "..");
	assert(push(&buf, "")! == "..");
	assert(push(&buf, ".")! == "..");
	assert(push(&buf, local("/")!)! == "..");

	assert(set(&buf)! == ".");
	// root dir invariants
	assert(push(&buf, local("/")!)! == local("/")!);
	assert(push(&buf, "")! == local("/")!);
	assert(push(&buf, ".")! == local("/")!);
	assert(push(&buf, "..")! == local("/")!);
	assert(push(&buf, local("/")!)! == local("/")!);

	assert(set(&buf)! == ".");
	// regular path and parent
	assert(push(&buf, "foo")! == "foo");
	assert(push(&buf, ".")! == "foo");
	assert(push(&buf, local("/")!)! == "foo");
	assert(push(&buf, "..")! == ".");

	// multiple segments
	assert(push(&buf, "a", "b")! == local("a/b")!);
	assert(push(&buf, "..", "c")! == local("a/c")!);
	assert(push(&buf, "..")! == "a");
	assert(push(&buf, local("/d")!)! == local("a/d")!);
	assert(push(&buf, "..", "..")! == ".");

	// multiple segments, absolute
	assert(push(&buf, local("/")!, "a", "b")! == local("/a/b")!);
	assert(push(&buf, "..", "c")! == local("/a/c")!);
	assert(push(&buf, "..")! == local("/a")!);
	assert(push(&buf, local("/d")!)! == local("/a/d")!);
	assert(push(&buf, "..", "..", "..")! == local("/")!);
};

// Examine the final path segment in a buffer.
// Returns void if the path is empty or is the root dir.
export fn peek(buf: *const buffer) (str | void) = split(buf).1;

// Remove and return the final path segment in a buffer.
// Returns void if the path is empty or is the root dir.
export fn pop(buf: *buffer) (str | void) = {
	const (end, res) = split(buf);
	buf.end = end;
	return res;
};

// helper function for pop/peek, returns (new end of buffer, result)
fn split(buf: *buffer) (size, (str | void)) = {
	if (buf.end == 0 || isroot(buf)) return (buf.end, void);
	match (bytes::rindex(buf.buf[..buf.end], SEP)) {
	case void =>
		return (0z, strings::fromutf8_unsafe(buf.buf[..buf.end]));
	case let i: size =>
		return (
			if (i == 0) 1z else i,
			strings::fromutf8_unsafe(buf.buf[i+1..buf.end]),
		);
	};
};

@test fn pop() void = {
	// empty
	let buf = init()!;
	assert(pop(&buf) is void);
	assert(string(&buf) == ".");

	// root dir
	buf.end = 0;
	push(&buf, local("/")!)!;
	assert(pop(&buf) is void);
	assert(string(&buf) == local("/")!);

	// relative file
	buf.end = 0;
	push(&buf, "foo")!;
	assert(pop(&buf) as str == "foo");
	assert(string(&buf) == ".");

	// abs file
	buf.end = 0;
	push(&buf, local("/foo")!)!;
	assert(pop(&buf) as str == "foo");
	assert(string(&buf) == local("/")!);
};

// Returns the parent directory for a given path, without modifying the buffer.
// If the path is the root directory, the root directory is returned. The value
// is either borrowed from the input or statically allocated; use
// [[strings::dup]] to extend its lifetime or modify it.
export fn parent(buf: *const buffer) (str | error) = {
	const newend = appendnorm(buf, dotdot)?;
	if (newend == 0) return ".";
	return strings::fromutf8_unsafe(buf.buf[..newend]);
};