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
//! Which ids may become filenames, and which are refused.
//!
//! **Refused, not rewritten.** The engine used to map every character outside
//! `[A-Za-z0-9._-]` to `_`, so `my session`, `my/session` and `my:session` all
//! became `my_session` — three distinct sessions quietly merging into one log
//! and one undo stack. Silent rewriting is what turns a typo into a collision,
//! and a collision between conversations is unrecoverable: by the time anyone
//! notices, two histories are interleaved in one file (D7).
//!
//! Rejection is honest about the same situation, and it is checkable. Once an
//! id is accepted it *is* its filename — no mapping, so no two ids can share
//! one.
//!
//! # The two namespaces
//!
//! Ids reach the store from two places: the host, which passes external
//! conversation and turn ids through unchanged (D6), and the engine, which
//! mints a turn id for the safety checkpoint taken before every restore. These
//! must not be able to collide, and previously did: the engine minted
//! `safety-restore:<id>`, which sanitized to `safety-restore_<id>` — a name a
//! user turn could hold exactly.
//!
//! They are disjoint by construction now. An internal id begins with
//! [`INTERNAL_PREFIX`]; an external one may not. Nothing is reserved by
//! convention or by luck.
use crateResult;
use crateSnapshotError;
/// What every internally minted id starts with, and no external id may.
///
/// A filename-safe character deliberately: the prefix has to survive to the
/// filesystem, so reserving one that needed escaping would reintroduce the
/// mapping this module exists to remove.
pub const INTERNAL_PREFIX: char = '_';
/// Whether `c` may appear in an id that becomes a filename.
///
/// Deliberately narrow. It excludes the path separators on every platform we
/// support, the drive-letter colon, and everything a shell would need quoted —
/// so an accepted id is safe to put in a path, print in a log line, and pass
/// to a command without further thought.
/// Prove `id` cannot resolve outside the directory it will live in.
///
/// This is the guarantee D5 asks for, and it is enforced where paths are
/// *built* rather than where they are accepted. An entry-point check is a
/// promise every future method has to remember to keep; a check inside the
/// path builder is one the type system asks for on every call.
///
/// `.` and `..` are the interesting cases. Both survive any character-level
/// filter — they contain nothing illegal — and both resolve to a directory
/// rather than a file in it. `..` reaches the partition root, and the empty
/// string resolves to the enclosing directory itself, which `with_extension`
/// then turns into a sibling of it: writing `turns/` as an id produced
/// `<partition>/turns.tmp`, one level above where it belonged.
pub
/// The longest id that is portable as a single path component. Every
/// filesystem we target allows at least 255 bytes per component; the record
/// suffix and the `.tmp` a write adds have to fit alongside it.
const MAX_ID_BYTES: usize = 200;
/// Prove `id` came from outside, and so may not claim the engine's namespace.
///
/// Everything [`validate_stored`] requires, plus the prefix rule. Applied at
/// the public API boundary: a host that hands us an id beginning with `_` is
/// told so rather than being allowed to shadow a safety checkpoint.
pub
/// Whether `name` is the shape of a content-addressed object: 64 lowercase
/// hex characters.
///
/// The whitelist D9 asks every enumeration to use. A blacklist of `.tmp` is
/// the same idea stated the fragile way round: it admits anything nobody
/// thought to exclude, which is how a half-written file became readable as a
/// record and then an uncollectable GC root (C4).
pub
/// Refuse an object id that is not the shape we mint.
///
/// Object ids are ours — a SHA-256 of content — so a malformed one means a
/// record on disk has been corrupted or forged. It matters because those ids
/// are read back out of manifests and handed straight to `remove`: an id of
/// `/etc/passwd` would resolve there, since joining an absolute path replaces
/// everything before it.
pub