@effect-vfs/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -31
- package/dist/BytePath.d.ts +114 -4
- package/dist/BytePath.d.ts.map +1 -1
- package/dist/BytePath.js +155 -0
- package/dist/Caller.d.ts +391 -0
- package/dist/Caller.d.ts.map +1 -0
- package/dist/Caller.js +199 -0
- package/dist/FileHandle.d.ts +50 -0
- package/dist/FileHandle.d.ts.map +1 -0
- package/dist/FileHandle.js +28 -0
- package/dist/Fixture.d.ts +155 -0
- package/dist/Fixture.d.ts.map +1 -0
- package/dist/Fixture.js +65 -0
- package/dist/LiveVolume.d.ts +11 -21
- package/dist/LiveVolume.d.ts.map +1 -1
- package/dist/LiveVolume.js +17 -14
- package/dist/Metadata.d.ts +186 -0
- package/dist/Metadata.d.ts.map +1 -0
- package/dist/Metadata.js +125 -0
- package/dist/Snapshot.d.ts +31 -53
- package/dist/Snapshot.d.ts.map +1 -1
- package/dist/Snapshot.js +26 -49
- package/dist/SnapshotDelta.d.ts +24 -39
- package/dist/SnapshotDelta.d.ts.map +1 -1
- package/dist/SnapshotDelta.js +14 -35
- package/dist/Target.d.ts +225 -0
- package/dist/Target.d.ts.map +1 -0
- package/dist/Target.js +92 -0
- package/dist/Testing.d.ts +128 -0
- package/dist/Testing.d.ts.map +1 -0
- package/dist/Testing.js +111 -0
- package/dist/VfsError.d.ts +199 -0
- package/dist/VfsError.d.ts.map +1 -0
- package/dist/VfsError.js +164 -0
- package/dist/VirtualFileSystem.d.ts +628 -347
- package/dist/VirtualFileSystem.d.ts.map +1 -1
- package/dist/VirtualFileSystem.js +448 -115
- package/dist/Volume.d.ts +211 -0
- package/dist/Volume.d.ts.map +1 -0
- package/dist/Volume.js +172 -0
- package/dist/Watch.d.ts +68 -0
- package/dist/Watch.d.ts.map +1 -0
- package/dist/Watch.js +46 -0
- package/dist/index.d.ts +56 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -1
- package/dist/internal/budget.d.ts +90 -0
- package/dist/internal/budget.d.ts.map +1 -0
- package/dist/internal/budget.js +70 -0
- package/dist/internal/bytePath.d.ts +2 -0
- package/dist/internal/bytePath.d.ts.map +1 -1
- package/dist/internal/bytePath.js +3 -2
- package/dist/internal/bytes.d.ts +3 -0
- package/dist/internal/bytes.d.ts.map +1 -1
- package/dist/internal/bytes.js +4 -2
- package/dist/internal/canonicalBase64.d.ts +1 -4
- package/dist/internal/canonicalBase64.d.ts.map +1 -1
- package/dist/internal/canonicalBase64.js +9 -13
- package/dist/internal/errors.d.ts +47 -5
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +80 -36
- package/dist/internal/fixture.d.ts +2 -2
- package/dist/internal/fixture.d.ts.map +1 -1
- package/dist/internal/fixture.js +57 -74
- package/dist/internal/hex128.d.ts +12 -0
- package/dist/internal/hex128.d.ts.map +1 -0
- package/dist/internal/hex128.js +7 -0
- package/dist/internal/hmac.d.ts +5 -0
- package/dist/internal/hmac.d.ts.map +1 -0
- package/dist/internal/hmac.js +150 -0
- package/dist/internal/image.d.ts +14 -117
- package/dist/internal/image.d.ts.map +1 -1
- package/dist/internal/image.js +137 -167
- package/dist/internal/inodeTable.d.ts +20 -0
- package/dist/internal/inodeTable.d.ts.map +1 -0
- package/dist/internal/inodeTable.js +48 -0
- package/dist/internal/lines.d.ts +36 -0
- package/dist/internal/lines.d.ts.map +1 -0
- package/dist/internal/lines.js +115 -0
- package/dist/internal/liveImage.d.ts +19 -278
- package/dist/internal/liveImage.d.ts.map +1 -1
- package/dist/internal/liveImage.js +75 -166
- package/dist/internal/merkle.d.ts +45 -0
- package/dist/internal/merkle.d.ts.map +1 -0
- package/dist/internal/merkle.js +179 -0
- package/dist/internal/metadata.d.ts +2 -13
- package/dist/internal/metadata.d.ts.map +1 -1
- package/dist/internal/metadata.js +5 -15
- package/dist/internal/overlayDiff.d.ts +2 -2
- package/dist/internal/overlayDiff.d.ts.map +1 -1
- package/dist/internal/overlayDiff.js +0 -3
- package/dist/internal/path.d.ts +17 -5
- package/dist/internal/path.d.ts.map +1 -1
- package/dist/internal/path.js +38 -24
- package/dist/internal/snapshotDelta.d.ts +61 -60
- package/dist/internal/snapshotDelta.d.ts.map +1 -1
- package/dist/internal/snapshotDelta.js +612 -504
- package/dist/internal/snapshotDeltaModel.d.ts.map +1 -1
- package/dist/internal/snapshotDeltaModel.js +0 -1
- package/dist/internal/stagedState.d.ts +11 -15
- package/dist/internal/stagedState.d.ts.map +1 -1
- package/dist/internal/stagedState.js +15 -59
- package/dist/internal/testSeams.d.ts +17 -0
- package/dist/internal/testSeams.d.ts.map +1 -0
- package/dist/internal/testSeams.js +20 -0
- package/dist/internal/tree.d.ts +225 -0
- package/dist/internal/tree.d.ts.map +1 -0
- package/dist/internal/tree.js +385 -0
- package/dist/internal/turnstile.d.ts +8 -0
- package/dist/internal/turnstile.d.ts.map +1 -0
- package/dist/internal/turnstile.js +43 -0
- package/dist/internal/virtualFileSystem.d.ts +39 -489
- package/dist/internal/virtualFileSystem.d.ts.map +1 -1
- package/dist/internal/virtualFileSystem.js +2112 -2465
- package/dist/internal/volumeState.d.ts +110 -0
- package/dist/internal/volumeState.d.ts.map +1 -0
- package/dist/internal/volumeState.js +204 -0
- package/dist/internal/watchHub.d.ts +15 -6
- package/dist/internal/watchHub.d.ts.map +1 -1
- package/dist/internal/watchHub.js +56 -42
- package/package.json +48 -4
- package/dist/VirtualFileSystemError.d.ts +0 -54
- package/dist/VirtualFileSystemError.d.ts.map +0 -1
- package/dist/VirtualFileSystemError.js +0 -49
- package/dist/internal/overlayContent.d.ts +0 -15
- package/dist/internal/overlayContent.d.ts.map +0 -1
- package/dist/internal/overlayContent.js +0 -29
- package/dist/internal/testHooks.d.ts +0 -20
- package/dist/internal/testHooks.d.ts.map +0 -1
- package/dist/internal/testHooks.js +0 -23
|
@@ -1,622 +1,730 @@
|
|
|
1
|
-
// Exact snapshot delta construction, inspection, serialization, and application.
|
|
2
1
|
import * as ByteSize from "effect/ByteSize";
|
|
3
|
-
import * as Crypto from "effect/Crypto";
|
|
4
2
|
import * as Effect from "effect/Effect";
|
|
5
3
|
import * as Encoding from "effect/Encoding";
|
|
6
|
-
import * as Fn from "effect/Function";
|
|
7
|
-
import * as Match from "effect/Match";
|
|
8
|
-
import * as Predicate from "effect/Predicate";
|
|
9
4
|
import * as Result from "effect/Result";
|
|
10
5
|
import * as Schema from "effect/Schema";
|
|
11
|
-
import {
|
|
12
|
-
import { SnapshotDeltaError, SnapshotDifference, SnapshotNodeKind } from "../SnapshotDelta.js";
|
|
6
|
+
import { SnapshotChange, SnapshotDifference, SnapshotNodeKind } from "../SnapshotDelta.js";
|
|
13
7
|
import { make as makeBytePath } from "./bytePath.js";
|
|
14
|
-
import { bytesOrder, sameBytes } from "./bytes.js";
|
|
8
|
+
import { bytesOrder, decodeUtf8, sameBytes } from "./bytes.js";
|
|
15
9
|
import { CanonicalBase64 } from "./canonicalBase64.js";
|
|
10
|
+
import { imageFailure, isEncodingIssue, issueSite, VfsError } from "./errors.js";
|
|
16
11
|
import * as Image from "./image.js";
|
|
17
|
-
import
|
|
12
|
+
import * as InodeTable from "./inodeTable.js";
|
|
13
|
+
import * as Merkle from "./merkle.js";
|
|
14
|
+
import { StoredMetadata } from "./metadata.js";
|
|
15
|
+
import { isAttachedBytes, isNameBytes, joinPath, nameBytes, NUL_BYTE, ROOT_PATH, SLASH_BYTE } from "./path.js";
|
|
18
16
|
import * as SnapshotDeltaModel from "./snapshotDeltaModel.js";
|
|
17
|
+
import { UNCHECKED } from "./tree.js";
|
|
18
|
+
import { byEntryName, getNode, Ino, MAX_INO, payloadOf, ROOT_INO, storedMetadata } from "./volumeState.js";
|
|
19
19
|
const FORMAT = "effect-vfs-delta";
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const DOT_BYTE = 46;
|
|
23
|
-
const MAX_NAME_BYTES = 255;
|
|
24
|
-
const NUL_BYTE = 0;
|
|
25
|
-
const SHA256_BYTES = 32;
|
|
26
|
-
const ROOT_PATH = new Uint8Array([SLASH_BYTE]);
|
|
20
|
+
const OPERATION = "snapshotDelta";
|
|
21
|
+
const RULE_CHECK = "@effect-vfs/core/deltaRuleCheck";
|
|
27
22
|
const Path = CanonicalBase64.Encoded;
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Schema.
|
|
34
|
-
|
|
35
|
-
paths: Schema.Array(Path),
|
|
36
|
-
metadata: Image.StoredMetadata,
|
|
37
|
-
payload: Payload
|
|
38
|
-
}),
|
|
39
|
-
Schema.Struct({
|
|
40
|
-
kind: Schema.Literal("symlink"),
|
|
41
|
-
paths: Schema.Array(Path),
|
|
42
|
-
metadata: Image.StoredMetadata,
|
|
43
|
-
payload: Payload
|
|
44
|
-
})
|
|
45
|
-
]);
|
|
46
|
-
const AddedChange = Schema.TaggedStruct("Added", { path: Path, kind: SnapshotNodeKind });
|
|
47
|
-
const RemovedChange = Schema.TaggedStruct("Removed", { path: Path, kind: SnapshotNodeKind });
|
|
48
|
-
const UpdatedChange = Schema.TaggedStruct("Updated", {
|
|
49
|
-
path: Path,
|
|
50
|
-
beforeKind: SnapshotNodeKind,
|
|
51
|
-
afterKind: SnapshotNodeKind,
|
|
52
|
-
differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1))
|
|
23
|
+
// Unchanged payloads come from the base; a new hard link points to the group's first changed path.
|
|
24
|
+
const InlineContent = Schema.TaggedStruct("Inline", { bytes: CanonicalBase64.Encoded });
|
|
25
|
+
const DeltaNode = Schema.TaggedUnion({
|
|
26
|
+
directory: { metadata: StoredMetadata },
|
|
27
|
+
file: { metadata: StoredMetadata, content: Schema.optionalKey(InlineContent) },
|
|
28
|
+
symlink: { metadata: StoredMetadata, target: Schema.optionalKey(CanonicalBase64.Encoded) },
|
|
29
|
+
link: { to: Path }
|
|
53
30
|
});
|
|
54
|
-
const Change = Schema.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const WireDeltaRecord = Schema.Union([
|
|
60
|
-
Schema.Struct({
|
|
61
|
-
kind: Schema.Literal("directory"),
|
|
62
|
-
paths: Schema.Array(Schema.String),
|
|
63
|
-
metadata: WireStoredMetadata
|
|
64
|
-
}),
|
|
65
|
-
Schema.Struct({
|
|
66
|
-
kind: Schema.Literal("file"),
|
|
67
|
-
paths: Schema.Array(Schema.String),
|
|
68
|
-
metadata: WireStoredMetadata,
|
|
69
|
-
payload: WirePayload
|
|
70
|
-
}),
|
|
71
|
-
Schema.Struct({
|
|
72
|
-
kind: Schema.Literal("symlink"),
|
|
73
|
-
paths: Schema.Array(Schema.String),
|
|
74
|
-
metadata: WireStoredMetadata,
|
|
75
|
-
payload: WirePayload
|
|
76
|
-
})
|
|
77
|
-
]);
|
|
78
|
-
const WireChange = Schema.Union([
|
|
79
|
-
Schema.TaggedStruct("Added", { path: Schema.String, kind: SnapshotNodeKind }),
|
|
80
|
-
Schema.TaggedStruct("Removed", { path: Schema.String, kind: SnapshotNodeKind }),
|
|
81
|
-
Schema.TaggedStruct("Updated", {
|
|
82
|
-
path: Schema.String,
|
|
31
|
+
const Change = Schema.TaggedUnion({
|
|
32
|
+
Added: { path: Path, kind: SnapshotNodeKind, node: DeltaNode },
|
|
33
|
+
Removed: { path: Path, kind: SnapshotNodeKind },
|
|
34
|
+
Updated: {
|
|
35
|
+
path: Path,
|
|
83
36
|
beforeKind: SnapshotNodeKind,
|
|
84
37
|
afterKind: SnapshotNodeKind,
|
|
85
|
-
differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1))
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const sameChanges = Schema.toEquivalence(Schema.Array(Change));
|
|
89
|
-
const VersionProbe = Schema.Struct({
|
|
90
|
-
format: Schema.Literal(FORMAT),
|
|
91
|
-
version: Schema.Unknown
|
|
38
|
+
differences: Schema.Array(SnapshotDifference).check(Schema.isMinLength(1)),
|
|
39
|
+
node: DeltaNode
|
|
40
|
+
}
|
|
92
41
|
});
|
|
93
42
|
const Document = Schema.Struct({
|
|
94
43
|
format: Schema.Literal(FORMAT),
|
|
95
44
|
version: Schema.Literal(1),
|
|
96
|
-
base:
|
|
97
|
-
|
|
45
|
+
base: CanonicalBase64.Encoded,
|
|
46
|
+
target: CanonicalBase64.Encoded,
|
|
98
47
|
changes: Schema.Array(Change)
|
|
99
48
|
});
|
|
100
|
-
const
|
|
49
|
+
const VersionProbe = Schema.Struct({
|
|
101
50
|
format: Schema.Literal(FORMAT),
|
|
102
|
-
version: Schema.
|
|
103
|
-
base: Schema.Struct({ algorithm: Schema.Literal(ALGORITHM), digest: Schema.String }),
|
|
104
|
-
records: Schema.Array(WireDeltaRecord),
|
|
105
|
-
changes: Schema.Array(WireChange)
|
|
51
|
+
version: Schema.Unknown
|
|
106
52
|
});
|
|
107
|
-
const timestampFields = new Set(["atimeNs", "mtimeNs", "ctimeNs", "birthtimeNs"]);
|
|
108
53
|
const differenceOrder = SnapshotDifference.literals;
|
|
109
|
-
const
|
|
110
|
-
const Json = Schema.fromJsonString(Schema.Unknown);
|
|
111
|
-
const JsonDocument = Schema.fromJsonString(Document);
|
|
54
|
+
const timestampFields = new Set(["atimeNs", "mtimeNs", "ctimeNs", "birthtimeNs"]);
|
|
112
55
|
const key = Encoding.encodeHex;
|
|
113
|
-
const join = (parent, name) => {
|
|
114
|
-
const out = new Uint8Array(parent.length + (parent.length === 1 ? 0 : 1) + name.length);
|
|
115
|
-
out.set(parent);
|
|
116
|
-
let offset = parent.length;
|
|
117
|
-
if (parent.length !== 1)
|
|
118
|
-
out[offset++] = SLASH_BYTE;
|
|
119
|
-
out.set(name, offset);
|
|
120
|
-
return out;
|
|
121
|
-
};
|
|
122
|
-
const parentKey = (path) => {
|
|
123
|
-
let slash = path.length - 1;
|
|
124
|
-
while (slash > 0 && path[slash] !== SLASH_BYTE)
|
|
125
|
-
slash--;
|
|
126
|
-
return key(slash === 0 ? ROOT_PATH : path.subarray(0, slash));
|
|
127
|
-
};
|
|
128
|
-
const basename = (path) => {
|
|
129
|
-
let slash = path.length - 1;
|
|
130
|
-
while (slash > 0 && path[slash] !== SLASH_BYTE)
|
|
131
|
-
slash--;
|
|
132
|
-
return path.subarray(slash + 1);
|
|
133
|
-
};
|
|
134
|
-
const safeAdd = (a, b) => {
|
|
135
|
-
const n = a + b;
|
|
136
|
-
return Number.isSafeInteger(n) ? n : undefined;
|
|
137
|
-
};
|
|
138
|
-
const exceeds = (value, limit) => ByteSize.isGreaterThan(ByteSize.bytes(value), limit);
|
|
139
56
|
const validPath = (path) => {
|
|
140
|
-
if (path
|
|
141
|
-
(path.length > 1 && path.at(-1) === SLASH_BYTE))
|
|
57
|
+
if (path[0] !== SLASH_BYTE)
|
|
142
58
|
return false;
|
|
143
|
-
if (path.length === 1)
|
|
144
|
-
return true;
|
|
145
59
|
let start = 1;
|
|
146
|
-
for (let i = 1; i <= path.length; i++) {
|
|
60
|
+
for (let i = 1; i <= path.length && path.length > 1; i++) {
|
|
147
61
|
if (i === path.length || path[i] === SLASH_BYTE) {
|
|
148
|
-
|
|
149
|
-
if (length < 1 || length > MAX_NAME_BYTES || (length === 1 && path[start] === DOT_BYTE) ||
|
|
150
|
-
(length === 2 && path[start] === DOT_BYTE && path[start + 1] === DOT_BYTE))
|
|
62
|
+
if (!isNameBytes(path.subarray(start, i)))
|
|
151
63
|
return false;
|
|
152
64
|
start = i + 1;
|
|
153
65
|
}
|
|
154
66
|
}
|
|
155
67
|
return true;
|
|
156
68
|
};
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
records: limits.maxTargetRecords,
|
|
168
|
-
recordsField: "targetRecords",
|
|
169
|
-
pathBytes: limits.maxDecodedDeltaBytes,
|
|
170
|
-
pathBytesField: "decodedDeltaBytes",
|
|
171
|
-
payloadBytes: limits.maxOutputBytes,
|
|
172
|
-
payloadBytesField: "outputBytes"
|
|
173
|
-
};
|
|
174
|
-
const encodedPayload = (record) => Match.value(record).pipe(Match.tag("directory", () => undefined), Match.tag("file", (record) => record.data), Match.tag("symlink", (record) => record.target), Match.exhaustive);
|
|
175
|
-
const normalize = Effect.fnUntraced(function* (snapshot, limits, role) {
|
|
176
|
-
const doc = yield* Image.inspect(snapshot);
|
|
177
|
-
const budget = roleBudget(limits, role);
|
|
178
|
-
if (doc.records.length > budget.records) {
|
|
179
|
-
return yield* new ImageError({ code: "LimitExceeded", field: budget.recordsField });
|
|
69
|
+
const splitNames = (path) => {
|
|
70
|
+
const names = [];
|
|
71
|
+
if (path.length === 1)
|
|
72
|
+
return names;
|
|
73
|
+
let start = 1;
|
|
74
|
+
for (let i = 1; i <= path.length; i++) {
|
|
75
|
+
if (i === path.length || path[i] === SLASH_BYTE) {
|
|
76
|
+
names.push(key(path.subarray(start, i)));
|
|
77
|
+
start = i + 1;
|
|
78
|
+
}
|
|
180
79
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
80
|
+
return names;
|
|
81
|
+
};
|
|
82
|
+
const kindOf = (change) => Change.guards.Updated(change) ? change.afterKind : change.kind;
|
|
83
|
+
const payloadDifference = (kind) => (kind === "file" ? "content" : "target");
|
|
84
|
+
const carriesPayload = (change) => Change.guards.Added(change) ||
|
|
85
|
+
(Change.guards.Updated(change) &&
|
|
86
|
+
(change.differences.includes("kind") || change.differences.includes(payloadDifference(change.afterKind))));
|
|
87
|
+
const carriedPayload = (node) => DeltaNode.guards.file(node) ? node.content?.bytes : DeltaNode.guards.symlink(node) ? node.target : undefined;
|
|
88
|
+
const changesIssue = (changes) => {
|
|
89
|
+
const at = (path, issue) => ({ path: ["changes", ...path], issue });
|
|
90
|
+
const heads = new Map();
|
|
91
|
+
let previous;
|
|
92
|
+
for (const [index, change] of changes.entries()) {
|
|
93
|
+
const path = CanonicalBase64.toBytes(change.path);
|
|
94
|
+
if (!validPath(path) || (previous !== undefined && bytesOrder(previous, path) >= 0)) {
|
|
95
|
+
return at([], "changes ascend by path bytes, each a valid path");
|
|
96
|
+
}
|
|
97
|
+
previous = path;
|
|
98
|
+
const root = path.length === 1;
|
|
99
|
+
if (Change.guards.Removed(change)) {
|
|
100
|
+
if (root)
|
|
101
|
+
return at([index], "the root is never removed");
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (Change.guards.Added(change) && root)
|
|
105
|
+
return at([index], "the root is never added");
|
|
106
|
+
if (Change.guards.Updated(change)) {
|
|
107
|
+
const positions = change.differences.map((field) => differenceOrder.indexOf(field));
|
|
108
|
+
if (positions.some((position, place) => place > 0 && position <= positions[place - 1])) {
|
|
109
|
+
return at([index, "differences"], "differences are listed once each in their canonical order");
|
|
110
|
+
}
|
|
111
|
+
if (change.differences.includes("kind") !== (change.beforeKind !== change.afterKind)) {
|
|
112
|
+
return at([index, "differences"], "an update lists kind exactly when the kind changes");
|
|
113
|
+
}
|
|
114
|
+
if (root && (change.beforeKind !== "directory" || change.afterKind !== "directory")) {
|
|
115
|
+
return at([index], "the root stays a directory");
|
|
208
116
|
}
|
|
209
117
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const paths = pathsById.get(record.id);
|
|
216
|
-
if (paths === undefined)
|
|
217
|
-
return yield* new ImageError({ code: "InvalidStructure", field: role });
|
|
218
|
-
paths.sort(bytesOrder);
|
|
219
|
-
const encoded = encodedPayload(record);
|
|
220
|
-
if (encoded !== undefined) {
|
|
221
|
-
payloadBytes = ByteSize.sum(payloadBytes, ByteSize.bytes(CanonicalBase64.decodedLength(encoded)));
|
|
222
|
-
if (ByteSize.isGreaterThan(payloadBytes, budget.payloadBytes)) {
|
|
223
|
-
return yield* new ImageError({ code: "LimitExceeded", field: budget.payloadBytesField });
|
|
118
|
+
const kind = kindOf(change);
|
|
119
|
+
const node = change.node;
|
|
120
|
+
if (DeltaNode.guards.link(node)) {
|
|
121
|
+
if (heads.get(node.to) !== kind) {
|
|
122
|
+
return at([index, "node", "to"], "a link names an earlier change's file or symbolic link of its kind");
|
|
224
123
|
}
|
|
124
|
+
continue;
|
|
225
125
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
126
|
+
if (node._tag !== kind)
|
|
127
|
+
return at([index, "node"], "a node matches its change's kind");
|
|
128
|
+
if (DeltaNode.guards.directory(node))
|
|
129
|
+
continue;
|
|
130
|
+
if ((carriedPayload(node) !== undefined) !== carriesPayload(change)) {
|
|
131
|
+
return at([index, "node"], "a node carries its payload exactly when the change alters it");
|
|
132
|
+
}
|
|
133
|
+
if (DeltaNode.guards.symlink(node) && node.target !== undefined &&
|
|
134
|
+
CanonicalBase64.toBytes(node.target).includes(NUL_BYTE)) {
|
|
135
|
+
return at([index, "node", "target"], "a symbolic link's target holds no NUL");
|
|
136
|
+
}
|
|
137
|
+
heads.set(change.path, kind);
|
|
236
138
|
}
|
|
237
|
-
objects.sort((a, b) => bytesOrder(a.paths[0], b.paths[0]));
|
|
238
|
-
return { objects, byPath };
|
|
239
|
-
});
|
|
240
|
-
const U64_BYTES = 8;
|
|
241
|
-
const EMPTY = new Uint8Array();
|
|
242
|
-
const IDENTITY_PREFIX = encoder.encode(`${ALGORITHM}\0`);
|
|
243
|
-
const KIND_BYTE = { directory: 0, file: 1, symlink: 2 };
|
|
244
|
-
const u64 = (n) => {
|
|
245
|
-
const out = new Uint8Array(U64_BYTES);
|
|
246
|
-
new DataView(out.buffer).setBigUint64(0, BigInt(n), false);
|
|
247
|
-
return out;
|
|
248
139
|
};
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
140
|
+
const ValidDocument = Document.check(Schema.makeFilter((document) => changesIssue(document.changes) ?? true, {
|
|
141
|
+
identifier: "ValidSnapshotDelta",
|
|
142
|
+
[RULE_CHECK]: true
|
|
143
|
+
}));
|
|
144
|
+
const JsonDocument = Schema.fromJsonString(Document);
|
|
145
|
+
const Json = Schema.fromJsonString(Schema.Unknown);
|
|
146
|
+
const decodeDocument = Schema.decodeUnknownEffect(ValidDocument, { onExcessProperty: "error" });
|
|
147
|
+
// Preserve the codec's stable error fields for digest, path, and payload failures.
|
|
148
|
+
const failureField = (path) => {
|
|
149
|
+
const [head, , field] = path;
|
|
150
|
+
if (head === "base" || head === "target")
|
|
151
|
+
return "digest";
|
|
152
|
+
if (head === "changes" && path.length === 3 && field === "path")
|
|
153
|
+
return "changePath";
|
|
154
|
+
if (head === "changes" && field === "node" && (path[3] === "target" || path.at(-1) === "bytes"))
|
|
155
|
+
return "payload";
|
|
156
|
+
return path.length === 0 ? "document" : path.map(String).join(".");
|
|
157
|
+
};
|
|
158
|
+
const decodeFailure = (error) => {
|
|
159
|
+
const site = issueSite(error.issue);
|
|
160
|
+
const rule = site.checks.some((check) => check.annotations?.[RULE_CHECK] === true);
|
|
161
|
+
return rule
|
|
162
|
+
? imageFailure(OPERATION, "InvalidStructure", { field: site.path.map(String).join("."), cause: error })
|
|
163
|
+
: imageFailure(OPERATION, isEncodingIssue(site) ? "InvalidEncoding" : "InvalidStructure", {
|
|
164
|
+
field: failureField(site.path),
|
|
165
|
+
cause: error
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const exceeds = (value, limit) => ByteSize.isGreaterThan(ByteSize.bytes(value), limit);
|
|
169
|
+
const validate = (document, limits) => {
|
|
170
|
+
const fail = (code, field) => Result.fail(imageFailure(OPERATION, code, { field }));
|
|
171
|
+
if (document.changes.length > limits.records)
|
|
172
|
+
return fail("LimitExceeded", "deltaRecords");
|
|
173
|
+
for (const digest of [document.base, document.target]) {
|
|
174
|
+
if (CanonicalBase64.decodedLength(digest) !== Merkle.DIGEST_BYTES)
|
|
175
|
+
return fail("InvalidEncoding", "digest");
|
|
266
176
|
}
|
|
267
|
-
|
|
268
|
-
let
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
put(u64(object.metadata.gid));
|
|
286
|
-
put(u64(object.metadata.mode));
|
|
287
|
-
for (const stamp of timestamps[index])
|
|
288
|
-
frame(stamp);
|
|
289
|
-
frame(object.payload ?? EMPTY);
|
|
177
|
+
let decoded = 2 * Merkle.DIGEST_BYTES;
|
|
178
|
+
let entries = 0;
|
|
179
|
+
let payloadBytes = 0;
|
|
180
|
+
for (const change of document.changes) {
|
|
181
|
+
const pathBytes = CanonicalBase64.decodedLength(change.path);
|
|
182
|
+
decoded += pathBytes;
|
|
183
|
+
if (Change.guards.Removed(change))
|
|
184
|
+
continue;
|
|
185
|
+
if (pathBytes > 1)
|
|
186
|
+
entries++;
|
|
187
|
+
if (DeltaNode.guards.link(change.node))
|
|
188
|
+
decoded += CanonicalBase64.decodedLength(change.node.to);
|
|
189
|
+
else {
|
|
190
|
+
const payload = carriedPayload(change.node);
|
|
191
|
+
const length = payload === undefined ? 0 : CanonicalBase64.decodedLength(payload);
|
|
192
|
+
decoded += length;
|
|
193
|
+
payloadBytes += length;
|
|
194
|
+
}
|
|
290
195
|
}
|
|
291
|
-
|
|
196
|
+
if (exceeds(decoded, limits.decodedBytes))
|
|
197
|
+
return fail("LimitExceeded", "decodedDeltaBytes");
|
|
198
|
+
if (entries > limits.entries)
|
|
199
|
+
return fail("LimitExceeded", "entries");
|
|
200
|
+
if (exceeds(payloadBytes, limits.outputBytes))
|
|
201
|
+
return fail("LimitExceeded", "outputBytes");
|
|
202
|
+
return Result.void;
|
|
292
203
|
};
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
204
|
+
const baseBudget = (limits) => ({
|
|
205
|
+
operation: OPERATION,
|
|
206
|
+
records: limits.baseRecords,
|
|
207
|
+
recordsField: "baseRecords",
|
|
208
|
+
entries: limits.entries,
|
|
209
|
+
identityBytes: limits.identityBytes
|
|
296
210
|
});
|
|
297
|
-
const
|
|
298
|
-
|
|
211
|
+
const targetBudget = (limits) => ({
|
|
212
|
+
operation: OPERATION,
|
|
213
|
+
records: limits.targetRecords,
|
|
214
|
+
recordsField: "targetRecords",
|
|
215
|
+
entries: limits.entries,
|
|
216
|
+
identityBytes: limits.identityBytes,
|
|
217
|
+
payloadBytes: { limit: limits.outputBytes, field: "outputBytes" }
|
|
218
|
+
});
|
|
219
|
+
// Check target bounds first so applying and diffing report the same limit.
|
|
220
|
+
const outputIssue = (limits, output) => {
|
|
221
|
+
const fail = (field) => Result.fail(imageFailure(OPERATION, "LimitExceeded", { field }));
|
|
222
|
+
if (output.records > limits.targetRecords)
|
|
223
|
+
return fail("targetRecords");
|
|
224
|
+
if (output.entries > limits.entries)
|
|
225
|
+
return fail("entries");
|
|
226
|
+
if (output.records > limits.outputRecords)
|
|
227
|
+
return fail("outputRecords");
|
|
228
|
+
if (exceeds(output.payloadBytes, limits.outputBytes))
|
|
229
|
+
return fail("outputBytes");
|
|
230
|
+
if (output.records - output.written > limits.inheritedRecords)
|
|
231
|
+
return fail("inheritedRecords");
|
|
232
|
+
return Result.void;
|
|
233
|
+
};
|
|
234
|
+
const groupKey = (paths) => paths.map(key).join("/");
|
|
235
|
+
const side = (value, identity) => {
|
|
236
|
+
const groupOf = new Map();
|
|
237
|
+
const firstOf = new Map();
|
|
238
|
+
for (const [ino, paths] of identity.groups) {
|
|
239
|
+
const group = groupKey(paths);
|
|
240
|
+
firstOf.set(ino, paths[0]);
|
|
241
|
+
for (const path of paths)
|
|
242
|
+
groupOf.set(key(path), group);
|
|
243
|
+
}
|
|
244
|
+
return { value, identity, groupOf, firstOf };
|
|
245
|
+
};
|
|
246
|
+
const groupAt = (side, pathKey) => side.groupOf.get(pathKey) ?? pathKey;
|
|
247
|
+
const differencesOf = (before, after, hardLinks) => differenceOrder.filter((field) => {
|
|
299
248
|
if (field === "kind")
|
|
300
249
|
return before.kind !== after.kind;
|
|
301
|
-
if (field === "content") {
|
|
302
|
-
return before.kind === after.kind && after.kind
|
|
303
|
-
|
|
304
|
-
if (field === "target") {
|
|
305
|
-
return before.kind === after.kind && after.kind === "symlink" && !sameBytes(before.payload, after.payload);
|
|
250
|
+
if (field === "content" || field === "target") {
|
|
251
|
+
return before.kind === after.kind && after.kind !== "directory" && payloadDifference(after.kind) === field &&
|
|
252
|
+
!sameBytes(payloadOf(before), payloadOf(after));
|
|
306
253
|
}
|
|
307
254
|
if (field === "hardLinks")
|
|
308
|
-
return
|
|
255
|
+
return hardLinks;
|
|
309
256
|
return before.metadata[field] !== after.metadata[field];
|
|
310
257
|
});
|
|
258
|
+
const deltaNode = (target, path, node, withPayload) => {
|
|
259
|
+
const first = target.firstOf.get(node.ino);
|
|
260
|
+
if (node.kind !== "directory" && first !== undefined && !sameBytes(first, path)) {
|
|
261
|
+
return DeltaNode.cases.link.make({ to: CanonicalBase64.encode(first) }, UNCHECKED);
|
|
262
|
+
}
|
|
263
|
+
const metadata = storedMetadata(node.metadata);
|
|
264
|
+
if (node.kind === "directory")
|
|
265
|
+
return DeltaNode.cases.directory.make({ metadata }, UNCHECKED);
|
|
266
|
+
if (!withPayload) {
|
|
267
|
+
return node.kind === "file"
|
|
268
|
+
? DeltaNode.cases.file.make({ metadata }, UNCHECKED)
|
|
269
|
+
: DeltaNode.cases.symlink.make({ metadata }, UNCHECKED);
|
|
270
|
+
}
|
|
271
|
+
const bytes = CanonicalBase64.encode(payloadOf(node));
|
|
272
|
+
return node.kind === "file"
|
|
273
|
+
? DeltaNode.cases.file.make({ metadata, content: InlineContent.make({ bytes }, UNCHECKED) }, UNCHECKED)
|
|
274
|
+
: DeltaNode.cases.symlink.make({ metadata, target: bytes }, UNCHECKED);
|
|
275
|
+
};
|
|
276
|
+
const lookup = (get, entriesOf, path) => {
|
|
277
|
+
let parent = ROOT_INO;
|
|
278
|
+
let node = get(ROOT_INO);
|
|
279
|
+
let name = "";
|
|
280
|
+
for (const component of splitNames(path)) {
|
|
281
|
+
if (node?.kind !== "directory")
|
|
282
|
+
return undefined;
|
|
283
|
+
const ino = entriesOf(node).get(component);
|
|
284
|
+
if (ino === undefined)
|
|
285
|
+
return undefined;
|
|
286
|
+
parent = node.ino;
|
|
287
|
+
name = component;
|
|
288
|
+
node = get(ino);
|
|
289
|
+
}
|
|
290
|
+
return node === undefined ? undefined : { parent, name, node };
|
|
291
|
+
};
|
|
292
|
+
const valueEntries = (directory) => directory.entries;
|
|
293
|
+
// Equal digests skip a subtree; hard-link groups still need a separate path comparison.
|
|
311
294
|
const compare = (base, target) => {
|
|
312
295
|
const changes = [];
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
296
|
+
const listed = new Set();
|
|
297
|
+
const pending = [];
|
|
298
|
+
const encode = CanonicalBase64.encode;
|
|
299
|
+
const children = (path, before, after) => {
|
|
300
|
+
const names = new Set([
|
|
301
|
+
...(before?.kind === "directory" ? before.entries.keys() : []),
|
|
302
|
+
...(after?.kind === "directory" ? after.entries.keys() : [])
|
|
303
|
+
]);
|
|
304
|
+
for (const name of names) {
|
|
305
|
+
const b = before?.kind === "directory" ? before.entries.get(name) : undefined;
|
|
306
|
+
const a = after?.kind === "directory" ? after.entries.get(name) : undefined;
|
|
307
|
+
pending.push([
|
|
308
|
+
joinPath(path, nameBytes(name)),
|
|
309
|
+
b === undefined ? undefined : getNode(base.value, b),
|
|
310
|
+
a === undefined ? undefined : getNode(target.value, a)
|
|
311
|
+
]);
|
|
320
312
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
313
|
+
};
|
|
314
|
+
const updated = (path, before, after, differences) => changes.push({
|
|
315
|
+
path,
|
|
316
|
+
change: Change.cases.Updated.make({
|
|
317
|
+
path: encode(path),
|
|
318
|
+
beforeKind: before.kind,
|
|
319
|
+
afterKind: after.kind,
|
|
320
|
+
differences,
|
|
321
|
+
node: deltaNode(target, path, after, differences.includes("kind") || differences.includes(payloadDifference(after.kind)))
|
|
322
|
+
}, UNCHECKED)
|
|
323
|
+
});
|
|
324
|
+
pending.push([ROOT_PATH, getNode(base.value, ROOT_INO), getNode(target.value, ROOT_INO)]);
|
|
325
|
+
while (pending.length > 0) {
|
|
326
|
+
const [path, before, after] = pending.pop();
|
|
327
|
+
const pathKey = key(path);
|
|
328
|
+
if (base.groupOf.has(pathKey) || target.groupOf.has(pathKey))
|
|
329
|
+
listed.add(pathKey);
|
|
330
|
+
if (before === undefined && after !== undefined) {
|
|
331
|
+
changes.push({
|
|
332
|
+
path,
|
|
333
|
+
change: Change.cases.Added.make({ path: encode(path), kind: after.kind, node: deltaNode(target, path, after, true) }, UNCHECKED)
|
|
334
|
+
});
|
|
335
|
+
children(path, undefined, after);
|
|
336
|
+
}
|
|
337
|
+
else if (before !== undefined && after === undefined) {
|
|
338
|
+
changes.push({ path, change: Change.cases.Removed.make({ path: encode(path), kind: before.kind }, UNCHECKED) });
|
|
339
|
+
children(path, before, undefined);
|
|
340
|
+
}
|
|
341
|
+
else if (before !== undefined && after !== undefined) {
|
|
342
|
+
const same = sameBytes(base.identity.digests.get(before.ino), target.identity.digests.get(after.ino));
|
|
343
|
+
const hardLinks = groupAt(base, pathKey) !== groupAt(target, pathKey);
|
|
344
|
+
if (same && !hardLinks)
|
|
345
|
+
continue;
|
|
346
|
+
const differences = differencesOf(before, after, hardLinks);
|
|
347
|
+
if (differences.length > 0)
|
|
348
|
+
updated(path, before, after, differences);
|
|
349
|
+
if (!same)
|
|
350
|
+
children(path, before, after);
|
|
331
351
|
}
|
|
332
352
|
}
|
|
333
|
-
for (const [
|
|
334
|
-
|
|
335
|
-
|
|
353
|
+
for (const s of [base, target]) {
|
|
354
|
+
for (const paths of s.identity.groups.values()) {
|
|
355
|
+
for (const path of paths) {
|
|
356
|
+
const pathKey = key(path);
|
|
357
|
+
if (listed.has(pathKey))
|
|
358
|
+
continue;
|
|
359
|
+
listed.add(pathKey);
|
|
360
|
+
if (groupAt(base, pathKey) === groupAt(target, pathKey))
|
|
361
|
+
continue;
|
|
362
|
+
const before = lookup((ino) => getNode(base.value, ino), valueEntries, path).node;
|
|
363
|
+
const after = lookup((ino) => getNode(target.value, ino), valueEntries, path).node;
|
|
364
|
+
updated(path, before, after, ["hardLinks"]);
|
|
365
|
+
}
|
|
336
366
|
}
|
|
337
367
|
}
|
|
338
|
-
changes.sort((a, b) => bytesOrder(a.path, b.path));
|
|
339
|
-
return changes.map(({ change }) => change);
|
|
368
|
+
return changes.sort((a, b) => bytesOrder(a.path, b.path)).map(({ change }) => change);
|
|
340
369
|
};
|
|
341
370
|
const getDocument = (delta) => Effect.suspend(() => {
|
|
342
371
|
const value = SnapshotDeltaModel.value(delta);
|
|
343
372
|
return value !== undefined && Schema.is(Document)(value)
|
|
344
373
|
? Effect.succeed(value)
|
|
345
|
-
: Effect.fail(
|
|
374
|
+
: Effect.fail(imageFailure(OPERATION, "InvalidStructure", { field: "delta" }));
|
|
346
375
|
});
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
376
|
+
// Detach deepest paths first, then attach from the root. Check each claimed change against the base: a matching
|
|
377
|
+
// target digest alone cannot detect unreachable children or forged hard-link groups.
|
|
378
|
+
const fold = (base, identity, document) => {
|
|
379
|
+
const fail = (index, field) => Result.fail(imageFailure(OPERATION, "InvalidStructure", {
|
|
380
|
+
field: field === undefined ? `changes.${index}` : `changes.${index}.${field}`
|
|
381
|
+
}));
|
|
382
|
+
const owner = Symbol();
|
|
383
|
+
const revision = base.revision + 1n;
|
|
384
|
+
let inodes = base.inodes;
|
|
385
|
+
let nextInode = base.nextInode;
|
|
386
|
+
let entries = base.entries;
|
|
387
|
+
let usedBytes = base.usedBytes;
|
|
388
|
+
let records = identity.records;
|
|
389
|
+
let payloadBytes = identity.payloadBytes;
|
|
390
|
+
let written = 0;
|
|
391
|
+
const drafts = new Map();
|
|
392
|
+
const dirty = new Set();
|
|
393
|
+
const removed = new Set();
|
|
394
|
+
const created = new Map();
|
|
395
|
+
const get = (ino) => InodeTable.get(inodes, ino);
|
|
396
|
+
const put = (ino, node) => {
|
|
397
|
+
inodes = InodeTable.set(inodes, ino, node, owner);
|
|
398
|
+
};
|
|
399
|
+
const entriesOf = (directory) => drafts.get(directory.ino) ?? directory.entries;
|
|
400
|
+
const editable = (directory) => {
|
|
401
|
+
let draft = drafts.get(directory);
|
|
402
|
+
if (draft === undefined) {
|
|
403
|
+
const node = get(directory);
|
|
404
|
+
draft = new Map(node?.kind === "directory" ? node.entries : []);
|
|
405
|
+
drafts.set(directory, draft);
|
|
406
|
+
}
|
|
407
|
+
dirty.add(directory);
|
|
408
|
+
return draft;
|
|
409
|
+
};
|
|
410
|
+
const placed = document.changes.map((change, index) => {
|
|
411
|
+
const path = CanonicalBase64.toBytes(change.path);
|
|
412
|
+
return { index, change, path, pathKey: key(path) };
|
|
413
|
+
});
|
|
414
|
+
const changed = new Map(placed.map(({ change, pathKey }) => [pathKey, change]));
|
|
415
|
+
const before = new Map();
|
|
416
|
+
for (const { change, index, path, pathKey } of placed) {
|
|
417
|
+
if (Change.guards.Added(change))
|
|
363
418
|
continue;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const paths = new Map();
|
|
381
|
-
for (const [index, record] of document.records.entries()) {
|
|
382
|
-
for (const encodedPath of record.paths) {
|
|
383
|
-
const bytes = yield* CanonicalBase64.decode(encodedPath);
|
|
384
|
-
paths.set(key(bytes), { id: String(index), kind: record.kind, bytes });
|
|
419
|
+
const found = lookup((ino) => getNode(base, ino), valueEntries, path);
|
|
420
|
+
if (found?.node.kind !== (Change.guards.Updated(change) ? change.beforeKind : change.kind))
|
|
421
|
+
return fail(index);
|
|
422
|
+
before.set(pathKey, found);
|
|
423
|
+
// Reject partial removal of hard links or directory contents.
|
|
424
|
+
for (const member of identity.groups.get(found.node.ino) ?? []) {
|
|
425
|
+
const other = changed.get(key(member));
|
|
426
|
+
if (other === undefined || Change.guards.Added(other))
|
|
427
|
+
return fail(index);
|
|
428
|
+
}
|
|
429
|
+
if (found.node.kind === "directory" && (Change.guards.Removed(change) || kindOf(change) !== "directory")) {
|
|
430
|
+
for (const name of found.node.entries.keys()) {
|
|
431
|
+
const child = changed.get(key(joinPath(path, nameBytes(name))));
|
|
432
|
+
if (child === undefined || !Change.guards.Removed(child))
|
|
433
|
+
return fail(index);
|
|
434
|
+
}
|
|
385
435
|
}
|
|
386
436
|
}
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
437
|
+
const inPlace = (change) => Change.guards.Updated(change) && change.beforeKind === "directory" && change.afterKind === "directory";
|
|
438
|
+
for (let index = placed.length - 1; index >= 0; index--) {
|
|
439
|
+
const { change, pathKey } = placed[index];
|
|
440
|
+
if (Change.guards.Added(change) || inPlace(change))
|
|
441
|
+
continue;
|
|
442
|
+
const { name, node, parent } = before.get(pathKey);
|
|
443
|
+
editable(parent).delete(name);
|
|
444
|
+
entries--;
|
|
445
|
+
const current = get(node.ino);
|
|
446
|
+
if (current === undefined)
|
|
447
|
+
continue;
|
|
448
|
+
if (current.kind === "directory") {
|
|
449
|
+
put(current.ino, undefined);
|
|
450
|
+
removed.add(current.ino);
|
|
451
|
+
records--;
|
|
401
452
|
continue;
|
|
402
453
|
}
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "baseReference" });
|
|
408
|
-
outputBytes = ByteSize.sum(outputBytes, ByteSize.bytes(payload.length));
|
|
409
|
-
if (ByteSize.isGreaterThan(outputBytes, limits.maxOutputBytes)) {
|
|
410
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "outputBytes" });
|
|
454
|
+
const links = current.links.filter((link) => !(link.parent === parent && link.name === name));
|
|
455
|
+
if (links.length > 0) {
|
|
456
|
+
put(current.ino, { ...current, links, metadata: { ...current.metadata, nlink: links.length } });
|
|
457
|
+
continue;
|
|
411
458
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}));
|
|
419
|
-
}
|
|
420
|
-
return yield* Image.capture({ format: "effect-vfs", version: 1, root: tree.root, records }, undefined, true);
|
|
421
|
-
});
|
|
422
|
-
const validate = Effect.fnUntraced(function* (document, limits) {
|
|
423
|
-
const deltaRecords = safeAdd(document.records.length, document.changes.length);
|
|
424
|
-
if (deltaRecords === undefined || deltaRecords > limits.maxDeltaRecords) {
|
|
425
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "deltaRecords" });
|
|
426
|
-
}
|
|
427
|
-
if (document.records.length > limits.maxOutputRecords) {
|
|
428
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "outputRecords" });
|
|
429
|
-
}
|
|
430
|
-
if (!CanonicalBase64.is(document.base.digest) ||
|
|
431
|
-
CanonicalBase64.decodedLength(document.base.digest) !== SHA256_BYTES) {
|
|
432
|
-
return yield* new ImageError({ code: "InvalidEncoding", field: "digest" });
|
|
459
|
+
const size = payloadOf(current).length;
|
|
460
|
+
put(current.ino, undefined);
|
|
461
|
+
removed.add(current.ino);
|
|
462
|
+
records--;
|
|
463
|
+
payloadBytes -= size;
|
|
464
|
+
usedBytes -= BigInt(size);
|
|
433
465
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
466
|
+
const after = new Map();
|
|
467
|
+
for (const { change, index, path, pathKey } of placed) {
|
|
468
|
+
if (Change.guards.Removed(change))
|
|
469
|
+
continue;
|
|
470
|
+
const node = change.node;
|
|
471
|
+
if (!DeltaNode.guards.link(node))
|
|
472
|
+
written++;
|
|
473
|
+
if (inPlace(change)) {
|
|
474
|
+
const current = get(before.get(pathKey).node.ino);
|
|
475
|
+
if (current?.kind !== "directory" || !DeltaNode.guards.directory(node))
|
|
476
|
+
return fail(index);
|
|
477
|
+
put(current.ino, { ...current, metadata: { ...current.metadata, ...node.metadata }, revision });
|
|
478
|
+
dirty.add(current.ino);
|
|
479
|
+
after.set(pathKey, current.ino);
|
|
480
|
+
continue;
|
|
448
481
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
entries++;
|
|
455
|
-
if (entries > limits.maxEntries)
|
|
456
|
-
return yield* new ImageError({ code: "LimitExceeded", field: "entries" });
|
|
457
|
-
const bytes = yield* CanonicalBase64.decode(encodedPath);
|
|
458
|
-
const pathKey = key(bytes);
|
|
459
|
-
if (!validPath(bytes) || paths.has(pathKey)) {
|
|
460
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "path" });
|
|
461
|
-
}
|
|
462
|
-
paths.set(pathKey, { id: String(index), kind: record.kind, bytes });
|
|
482
|
+
const name = splitNames(path).at(-1);
|
|
483
|
+
const parentPath = path.subarray(0, Math.max(1, path.length - name.length / 2 - 1));
|
|
484
|
+
const parent = lookup(get, entriesOf, parentPath)?.node;
|
|
485
|
+
if (parent?.kind !== "directory") {
|
|
486
|
+
return Result.fail(imageFailure(OPERATION, "InvalidStructure", { field: "parent" }));
|
|
463
487
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
488
|
+
const draft = editable(parent.ino);
|
|
489
|
+
if (draft.has(name))
|
|
490
|
+
return fail(index);
|
|
491
|
+
const link = { parent: parent.ino, name };
|
|
492
|
+
let ino;
|
|
493
|
+
if (DeltaNode.guards.link(node)) {
|
|
494
|
+
const head = after.get(key(CanonicalBase64.toBytes(node.to)));
|
|
495
|
+
const target = head === undefined ? undefined : get(head);
|
|
496
|
+
if (target === undefined || target.kind === "directory")
|
|
497
|
+
return fail(index);
|
|
498
|
+
ino = target.ino;
|
|
499
|
+
const links = [...target.links, link];
|
|
500
|
+
put(ino, { ...target, links, metadata: { ...target.metadata, nlink: links.length } });
|
|
501
|
+
created.get(ino)?.push(path);
|
|
472
502
|
}
|
|
473
503
|
else {
|
|
474
|
-
if (
|
|
475
|
-
return
|
|
504
|
+
if (nextInode > MAX_INO)
|
|
505
|
+
return Result.fail(imageFailure(OPERATION, "LimitExceeded", { field: "inodes" }));
|
|
506
|
+
ino = nextInode;
|
|
507
|
+
nextInode = Ino(nextInode + 1);
|
|
508
|
+
const numbered = { ...node.metadata, ino: BigInt(ino) };
|
|
509
|
+
if (DeltaNode.guards.directory(node)) {
|
|
510
|
+
const metadata = { ...numbered, kind: "directory", nlink: 2, size: 0n };
|
|
511
|
+
put(ino, { kind: "directory", ino, parent: parent.ino, name, entries: new Map(), metadata, revision });
|
|
476
512
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
513
|
+
else {
|
|
514
|
+
const carried = carriedPayload(node);
|
|
515
|
+
const kept = before.get(pathKey)?.node;
|
|
516
|
+
const bytes = carried !== undefined
|
|
517
|
+
? CanonicalBase64.toBytes(carried)
|
|
518
|
+
: kept === undefined
|
|
519
|
+
? undefined
|
|
520
|
+
: payloadOf(kept);
|
|
521
|
+
if (bytes === undefined)
|
|
522
|
+
return fail(index);
|
|
523
|
+
const counted = { ...numbered, kind: node._tag, nlink: 1, size: BigInt(bytes.length) };
|
|
524
|
+
put(ino, DeltaNode.guards.file(node)
|
|
525
|
+
? { kind: "file", ino, data: bytes, links: [link], metadata: counted, revision }
|
|
526
|
+
: { kind: "symlink", ino, target: bytes, links: [link], metadata: counted, revision });
|
|
527
|
+
payloadBytes += bytes.length;
|
|
528
|
+
usedBytes += BigInt(bytes.length);
|
|
484
529
|
}
|
|
530
|
+
records++;
|
|
531
|
+
created.set(ino, [path]);
|
|
532
|
+
dirty.add(ino);
|
|
485
533
|
}
|
|
534
|
+
draft.set(name, ino);
|
|
535
|
+
entries++;
|
|
536
|
+
after.set(pathKey, ino);
|
|
486
537
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
538
|
+
const baseGroup = (ino, pathKey) => {
|
|
539
|
+
const paths = identity.groups.get(ino);
|
|
540
|
+
return paths === undefined ? pathKey : groupKey(paths);
|
|
541
|
+
};
|
|
542
|
+
const targetGroup = (ino, pathKey) => {
|
|
543
|
+
const paths = created.get(ino);
|
|
544
|
+
return paths === undefined || paths.length < 2 ? pathKey : groupKey([...paths].sort(bytesOrder));
|
|
545
|
+
};
|
|
546
|
+
for (const { change, index, pathKey } of placed) {
|
|
547
|
+
if (!Change.guards.Updated(change))
|
|
548
|
+
continue;
|
|
549
|
+
const was = before.get(pathKey).node;
|
|
550
|
+
const now = get(after.get(pathKey));
|
|
551
|
+
const found = differencesOf(was, now, baseGroup(was.ino, pathKey) !== targetGroup(now.ino, pathKey));
|
|
552
|
+
if (found.length !== change.differences.length || found.some((field, at) => field !== change.differences[at])) {
|
|
553
|
+
return fail(index, "differences");
|
|
496
554
|
}
|
|
497
|
-
previous = path;
|
|
498
555
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
yield* Effect.fromResult(linkTree(paths));
|
|
503
|
-
// Inline symlink targets are the only applied payloads a base snapshot has not already vetted.
|
|
504
|
-
for (const record of document.records) {
|
|
505
|
-
if (record.kind !== "symlink" || !Predicate.isTagged("Inline")(record.payload))
|
|
556
|
+
for (const [ino, draft] of drafts) {
|
|
557
|
+
const node = get(ino);
|
|
558
|
+
if (node?.kind !== "directory")
|
|
506
559
|
continue;
|
|
507
|
-
|
|
508
|
-
|
|
560
|
+
const sorted = [...draft].sort(byEntryName);
|
|
561
|
+
const subdirectories = sorted.filter(([, child]) => get(child)?.kind === "directory").length;
|
|
562
|
+
const metadata = { ...node.metadata, nlink: 2 + subdirectories };
|
|
563
|
+
put(ino, { ...node, entries: new Map(sorted), metadata, revision });
|
|
564
|
+
}
|
|
565
|
+
for (const ino of dirty) {
|
|
566
|
+
let node = get(ino);
|
|
567
|
+
if (node === undefined)
|
|
568
|
+
dirty.delete(ino);
|
|
569
|
+
while (node !== undefined && node.ino !== ROOT_INO) {
|
|
570
|
+
const parent = node.kind === "directory" ? node.parent : node.links[0].parent;
|
|
571
|
+
dirty.add(node.ino);
|
|
572
|
+
if (dirty.has(parent))
|
|
573
|
+
break;
|
|
574
|
+
dirty.add(parent);
|
|
575
|
+
node = get(parent);
|
|
509
576
|
}
|
|
510
|
-
|
|
511
|
-
|
|
577
|
+
}
|
|
578
|
+
const groups = new Map();
|
|
579
|
+
for (const [ino, paths] of identity.groups)
|
|
580
|
+
if (get(ino) !== undefined)
|
|
581
|
+
groups.set(ino, paths);
|
|
582
|
+
for (const [ino, paths] of created)
|
|
583
|
+
if (paths.length > 1)
|
|
584
|
+
groups.set(ino, [...paths].sort(bytesOrder));
|
|
585
|
+
const value = {
|
|
586
|
+
inodes,
|
|
587
|
+
open: new Map(),
|
|
588
|
+
nextInode,
|
|
589
|
+
revision: placed.length === 0 ? base.revision : revision,
|
|
590
|
+
entries,
|
|
591
|
+
usedBytes
|
|
592
|
+
};
|
|
593
|
+
return Result.succeed({ value, dirty, removed, groups, records, entries, payloadBytes, written });
|
|
594
|
+
};
|
|
595
|
+
// Reuse base digests for untouched nodes, but charge the meter for the whole target.
|
|
596
|
+
const foldedIdentity = Effect.fnUntraced(function* (base, folded, identity, limits) {
|
|
597
|
+
const digests = new Map();
|
|
598
|
+
const measure = Merkle.meter(targetBudget(limits));
|
|
599
|
+
let inherited = identity.nodeBytes;
|
|
600
|
+
for (const ino of new Set([...folded.dirty, ...folded.removed])) {
|
|
601
|
+
const was = identity.digests.has(ino) ? getNode(base, ino) : undefined;
|
|
602
|
+
if (was !== undefined)
|
|
603
|
+
inherited -= Merkle.nodeBytes(was);
|
|
604
|
+
}
|
|
605
|
+
yield* measure.charge(inherited);
|
|
606
|
+
const digestOf = (ino) => digests.get(ino) ?? identity.digests.get(ino);
|
|
607
|
+
const nodes = [...folded.dirty].map((ino) => getNode(folded.value, ino));
|
|
608
|
+
const depths = new Map([[ROOT_INO, 0]]);
|
|
609
|
+
const depth = (node) => {
|
|
610
|
+
const chain = [];
|
|
611
|
+
let current = node;
|
|
612
|
+
while (current?.kind === "directory" && !depths.has(current.ino)) {
|
|
613
|
+
chain.push(current.ino);
|
|
614
|
+
current = getNode(folded.value, current.parent);
|
|
512
615
|
}
|
|
616
|
+
let found = current === undefined ? -1 : depths.get(current.ino) ?? -1;
|
|
617
|
+
for (const ino of chain.reverse())
|
|
618
|
+
depths.set(ino, ++found);
|
|
619
|
+
return depths.get(node.ino);
|
|
620
|
+
};
|
|
621
|
+
const directories = nodes.filter((node) => node.kind === "directory");
|
|
622
|
+
// Digest children before their directories.
|
|
623
|
+
const ordered = [
|
|
624
|
+
...nodes.filter((node) => node.kind !== "directory"),
|
|
625
|
+
...directories.sort((a, b) => depth(b) - depth(a))
|
|
626
|
+
];
|
|
627
|
+
for (const node of ordered)
|
|
628
|
+
digests.set(node.ino, yield* Merkle.nodeDigest(node, digestOf, measure));
|
|
629
|
+
return yield* Merkle.identityDigest(digestOf(ROOT_INO), folded.groups.values(), measure);
|
|
630
|
+
});
|
|
631
|
+
const verify = Effect.fnUntraced(function* (base, document, limits) {
|
|
632
|
+
yield* Effect.fromResult(validate(document, limits));
|
|
633
|
+
const value = yield* Image.valueOf(base);
|
|
634
|
+
const identity = yield* Merkle.identify(value, baseBudget(limits));
|
|
635
|
+
if (!sameBytes(identity.digest, CanonicalBase64.toBytes(document.base))) {
|
|
636
|
+
return yield* new VfsError({ code: "BaseMismatch", operation: OPERATION });
|
|
513
637
|
}
|
|
638
|
+
const folded = yield* Effect.fromResult(fold(value, identity, document));
|
|
639
|
+
yield* Effect.fromResult(outputIssue(limits, folded));
|
|
640
|
+
if (!sameBytes(yield* foldedIdentity(value, folded, identity, limits), CanonicalBase64.toBytes(document.target))) {
|
|
641
|
+
return yield* imageFailure(OPERATION, "InvalidStructure", { field: "changes" });
|
|
642
|
+
}
|
|
643
|
+
return folded.value;
|
|
514
644
|
});
|
|
515
645
|
/** @internal */
|
|
516
646
|
export const diffSnapshots = Effect.fnUntraced(function* (base, target, limits) {
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
647
|
+
const baseValue = yield* Image.valueOf(base);
|
|
648
|
+
const targetValue = yield* Image.valueOf(target);
|
|
649
|
+
const before = side(baseValue, yield* Merkle.identify(baseValue, baseBudget(limits)));
|
|
650
|
+
const after = side(targetValue, yield* Merkle.identify(targetValue, targetBudget(limits)));
|
|
651
|
+
const changes = compare(before, after);
|
|
652
|
+
if (changes.length > limits.records) {
|
|
653
|
+
return yield* imageFailure(OPERATION, "LimitExceeded", { field: "deltaRecords" });
|
|
523
654
|
}
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
const paths = object.paths.map(CanonicalBase64.encode);
|
|
527
|
-
if (object.kind === "directory")
|
|
528
|
-
return { kind: object.kind, paths, metadata: object.metadata };
|
|
529
|
-
const inherited = object.paths.find((path) => {
|
|
530
|
-
const source = before.byPath.get(key(path));
|
|
531
|
-
return source?.object.kind === object.kind && samePayload(source.object)(object);
|
|
532
|
-
});
|
|
533
|
-
return {
|
|
534
|
-
kind: object.kind,
|
|
535
|
-
paths,
|
|
536
|
-
metadata: object.metadata,
|
|
537
|
-
payload: inherited === undefined
|
|
538
|
-
? InlinePayload.make({ bytes: CanonicalBase64.encode(object.payload ?? new Uint8Array()) })
|
|
539
|
-
: BasePayload.make({ path: CanonicalBase64.encode(inherited) })
|
|
540
|
-
};
|
|
541
|
-
});
|
|
655
|
+
const written = changes.filter((change) => !Change.guards.Removed(change) && !DeltaNode.guards.link(change.node));
|
|
656
|
+
yield* Effect.fromResult(outputIssue(limits, { ...after.identity, entries: targetValue.entries, written: written.length }));
|
|
542
657
|
const document = {
|
|
543
658
|
format: FORMAT,
|
|
544
659
|
version: 1,
|
|
545
|
-
base:
|
|
546
|
-
|
|
660
|
+
base: CanonicalBase64.encode(before.identity.digest),
|
|
661
|
+
target: CanonicalBase64.encode(after.identity.digest),
|
|
547
662
|
changes
|
|
548
663
|
};
|
|
549
|
-
yield* validate(document, limits);
|
|
664
|
+
yield* Effect.fromResult(validate(document, limits));
|
|
550
665
|
return SnapshotDeltaModel.make(document);
|
|
551
666
|
});
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const target = yield* buildImage(document, before, limits);
|
|
560
|
-
const changes = compare(before, yield* normalize(target, limits, "target"));
|
|
561
|
-
if (!sameChanges(changes, document.changes)) {
|
|
562
|
-
return yield* new ImageError({ code: "InvalidStructure", field: "changes" });
|
|
667
|
+
const [AddedSummary, RemovedSummary, UpdatedSummary] = SnapshotChange.members;
|
|
668
|
+
const publicChange = (change, options) => {
|
|
669
|
+
const path = makeBytePath(CanonicalBase64.toBytes(change.path));
|
|
670
|
+
if (Change.guards.Added(change))
|
|
671
|
+
return Object.freeze(AddedSummary.make({ path, kind: change.kind }, UNCHECKED));
|
|
672
|
+
if (Change.guards.Removed(change)) {
|
|
673
|
+
return Object.freeze(RemovedSummary.make({ path, kind: change.kind }, UNCHECKED));
|
|
563
674
|
}
|
|
564
|
-
|
|
565
|
-
|
|
675
|
+
const differences = change.differences.filter((field) => options.includeTimestamps === true || !timestampFields.has(field));
|
|
676
|
+
if (differences.length === 0)
|
|
677
|
+
return undefined;
|
|
678
|
+
return Object.freeze(UpdatedSummary.make({
|
|
679
|
+
path,
|
|
680
|
+
beforeKind: change.beforeKind,
|
|
681
|
+
afterKind: change.afterKind,
|
|
682
|
+
differences: Object.freeze(differences)
|
|
683
|
+
}, UNCHECKED));
|
|
684
|
+
};
|
|
566
685
|
/** @internal */
|
|
567
686
|
export const inspectSnapshotDelta = Effect.fnUntraced(function* (base, delta, options, limits) {
|
|
687
|
+
const document = yield* getDocument(delta);
|
|
688
|
+
yield* verify(base, document, limits);
|
|
568
689
|
const output = [];
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
output.push(Object.freeze({ ...change, path }));
|
|
574
|
-
else {
|
|
575
|
-
const fields = change.differences.filter((field) => options.includeTimestamps === true || !timestampFields.has(field));
|
|
576
|
-
if (fields.length > 0)
|
|
577
|
-
output.push(Object.freeze({ ...change, path, differences: Object.freeze([...fields]) }));
|
|
578
|
-
}
|
|
690
|
+
for (const change of document.changes) {
|
|
691
|
+
const shown = publicChange(change, options);
|
|
692
|
+
if (shown !== undefined)
|
|
693
|
+
output.push(shown);
|
|
579
694
|
}
|
|
580
695
|
return Object.freeze(output);
|
|
581
696
|
});
|
|
582
697
|
/** @internal */
|
|
698
|
+
export const applySnapshotDelta = Effect.fnUntraced(function* (base, delta, limits) {
|
|
699
|
+
return Image.make(yield* verify(base, yield* getDocument(delta), limits));
|
|
700
|
+
});
|
|
701
|
+
const encoder = new TextEncoder();
|
|
702
|
+
/** @internal */
|
|
583
703
|
export const encodeSnapshotDelta = Effect.fnUntraced(function* (delta, limits) {
|
|
584
704
|
const document = yield* getDocument(delta);
|
|
585
|
-
yield* validate(document, limits);
|
|
586
|
-
const text = yield* Schema.encodeEffect(JsonDocument)(document).pipe(Effect.mapError(() =>
|
|
705
|
+
yield* Effect.fromResult(validate(document, limits));
|
|
706
|
+
const text = yield* Schema.encodeEffect(JsonDocument)(document).pipe(Effect.mapError((cause) => imageFailure(OPERATION, "InvalidStructure", { cause })));
|
|
587
707
|
const bytes = encoder.encode(text);
|
|
588
|
-
if (exceeds(bytes.length, limits.
|
|
589
|
-
return yield*
|
|
708
|
+
if (exceeds(bytes.length, limits.encodedBytes)) {
|
|
709
|
+
return yield* imageFailure(OPERATION, "LimitExceeded", { field: "encodedBytes" });
|
|
590
710
|
}
|
|
591
711
|
return bytes;
|
|
592
712
|
});
|
|
593
713
|
/** @internal */
|
|
594
714
|
export const decodeSnapshotDelta = Effect.fnUntraced(function* (input, limits) {
|
|
595
|
-
if (!(input
|
|
596
|
-
return yield*
|
|
715
|
+
if (!isAttachedBytes(input)) {
|
|
716
|
+
return yield* imageFailure(OPERATION, "InvalidEncoding");
|
|
597
717
|
}
|
|
598
|
-
if (exceeds(input.byteLength, limits.
|
|
599
|
-
return yield*
|
|
718
|
+
if (exceeds(input.byteLength, limits.encodedBytes)) {
|
|
719
|
+
return yield* imageFailure(OPERATION, "LimitExceeded", { field: "encodedBytes" });
|
|
600
720
|
}
|
|
601
|
-
const text = yield*
|
|
602
|
-
|
|
603
|
-
catch: () => new ImageError({ code: "InvalidEncoding" })
|
|
604
|
-
});
|
|
605
|
-
const value = yield* Schema.decodeEffect(Json)(text).pipe(Effect.mapError(() => new ImageError({ code: "InvalidEncoding" })));
|
|
721
|
+
const text = yield* decodeUtf8(input, (cause) => imageFailure(OPERATION, "InvalidEncoding", { cause }));
|
|
722
|
+
const value = yield* Schema.decodeEffect(Json)(text).pipe(Effect.mapError((cause) => imageFailure(OPERATION, "InvalidEncoding", { cause })));
|
|
606
723
|
const version = Schema.decodeUnknownResult(VersionProbe)(value);
|
|
607
724
|
if (Result.isSuccess(version) && version.success.version !== 1) {
|
|
608
|
-
return yield*
|
|
725
|
+
return yield* imageFailure(OPERATION, "UnsupportedVersion");
|
|
609
726
|
}
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
yield* validate(wire.success, limits);
|
|
614
|
-
const parsed = Schema.decodeUnknownResult(Document, { onExcessProperty: "error" })(value);
|
|
615
|
-
if (Result.isFailure(parsed))
|
|
616
|
-
return yield* new ImageError({ code: "InvalidEncoding" });
|
|
617
|
-
return SnapshotDeltaModel.make(parsed.success);
|
|
618
|
-
});
|
|
619
|
-
/** @internal */
|
|
620
|
-
export const applySnapshotDelta = Effect.fnUntraced(function* (base, delta, limits) {
|
|
621
|
-
return (yield* verify(base, yield* getDocument(delta), limits)).target;
|
|
727
|
+
const document = yield* Effect.mapError(decodeDocument(value), decodeFailure);
|
|
728
|
+
yield* Effect.fromResult(validate(document, limits));
|
|
729
|
+
return SnapshotDeltaModel.make(document);
|
|
622
730
|
});
|