@mh-gg/git 0.1.1-alpha.20260626T104441232Z
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/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/git-remote-matterhorn.cjs +7 -0
- package/bin/matterhorn-git.cjs +297 -0
- package/package.json +38 -0
- package/src/cas/pointer.cjs +121 -0
- package/src/cas/storage.cjs +389 -0
- package/src/cas.cjs +26 -0
- package/src/chunker.cjs +67 -0
- package/src/controlPlane.cjs +257 -0
- package/src/crypto.cjs +213 -0
- package/src/index.cjs +11 -0
- package/src/localMirror.cjs +121 -0
- package/src/manifest.cjs +60 -0
- package/src/originUrl.cjs +163 -0
- package/src/remoteHelper.cjs +85 -0
- package/src/signing.cjs +110 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { canonicalJson } = require("@mh-gg/event/canonicalJson");
|
|
4
|
+
const { DEFAULT_CHUNKER, chunkBuffer, normalizeChunker } = require("./chunker.cjs");
|
|
5
|
+
const {
|
|
6
|
+
base64url,
|
|
7
|
+
chunkAad,
|
|
8
|
+
deriveEpochSubkeys,
|
|
9
|
+
fromBase64url,
|
|
10
|
+
hmacSha256Hex,
|
|
11
|
+
manifestAad,
|
|
12
|
+
parseSecret,
|
|
13
|
+
randomBytes,
|
|
14
|
+
sha256Hex,
|
|
15
|
+
xChaCha20Poly1305Open,
|
|
16
|
+
xChaCha20Poly1305Seal
|
|
17
|
+
} = require("./crypto.cjs");
|
|
18
|
+
const { createLogicalManifestIdentity, createPointer, parsePointer } = require("./manifest.cjs");
|
|
19
|
+
const { dataRootFor, gitDirFromEnv, writeJsonFile } = require("./localMirror.cjs");
|
|
20
|
+
const { originSlug, parseMatterhornOrigin } = require("./originUrl.cjs");
|
|
21
|
+
|
|
22
|
+
const DEFAULT_PROFILE = "dedup-same-epoch/encrypted-manifest/no-padding";
|
|
23
|
+
|
|
24
|
+
function ensurePrivateDir(dir) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
+
try { fs.chmodSync(dir, 0o700); } catch (matterhornIgnoredError) {
|
|
27
|
+
globalThis.__matterhornIgnoredError?.(matterhornIgnoredError, "packages/matterhorn-git/src/cas/storage.cjs");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function safeId(value) {
|
|
32
|
+
return String(value || "").replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 240) || "object";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function casRootFor(originInput, options = {}) {
|
|
36
|
+
const origin = parseMatterhornOrigin(originInput);
|
|
37
|
+
const gitDir = options.gitDir || gitDirFromEnv(options.cwd);
|
|
38
|
+
return path.join(dataRootFor(gitDir), "cas", originSlug(origin));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function keyRootFor(originInput, options = {}) {
|
|
42
|
+
const origin = parseMatterhornOrigin(originInput);
|
|
43
|
+
const gitDir = options.gitDir || gitDirFromEnv(options.cwd);
|
|
44
|
+
return path.join(dataRootFor(gitDir), "keys", originSlug(origin));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function indexFileFor(originInput, options = {}) {
|
|
48
|
+
return path.join(casRootFor(originInput, options), "index.json");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function objectPathFor(originInput, objectId, options = {}) {
|
|
52
|
+
const root = casRootFor(originInput, options);
|
|
53
|
+
const id = safeId(objectId);
|
|
54
|
+
return path.join(root, "objects", id.slice(0, 12), `${id}.json`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function manifestPathFor(originInput, objectId, options = {}) {
|
|
58
|
+
const root = casRootFor(originInput, options);
|
|
59
|
+
const id = safeId(objectId);
|
|
60
|
+
return path.join(root, "manifests", id.slice(0, 12), `${id}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function epochRootFileFor(originInput, epoch = 1, options = {}) {
|
|
64
|
+
return path.join(keyRootFor(originInput, options), `epoch-${Number(epoch) || 1}.key`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readJson(file, fallback) {
|
|
68
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readCasIndex(originInput, options = {}) {
|
|
72
|
+
return readJson(indexFileFor(originInput, options), {
|
|
73
|
+
version: 1,
|
|
74
|
+
chunks: {},
|
|
75
|
+
manifests: {},
|
|
76
|
+
manifestObjects: {},
|
|
77
|
+
objects: {}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeCasIndex(originInput, index, options = {}) {
|
|
82
|
+
writeJsonFile(indexFileFor(originInput, options), JSON.parse(canonicalJson({ version: 1, ...index })));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function envEpochRoot(epoch) {
|
|
86
|
+
return process.env[`MATTERHORN_GIT_EPOCH_ROOT_${Number(epoch) || 1}`] || process.env.MATTERHORN_GIT_EPOCH_ROOT;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadOrCreateEpochRoot(originInput, epoch = 1, options = {}) {
|
|
90
|
+
if (options.epochRoot) return parseSecret(options.epochRoot, "epochRoot");
|
|
91
|
+
const fromEnv = envEpochRoot(epoch);
|
|
92
|
+
if (fromEnv) return parseSecret(fromEnv, "MATTERHORN_GIT_EPOCH_ROOT");
|
|
93
|
+
const file = epochRootFileFor(originInput, epoch, options);
|
|
94
|
+
try {
|
|
95
|
+
return parseSecret(fs.readFileSync(file, "utf8"), file);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (!error || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) throw error;
|
|
98
|
+
}
|
|
99
|
+
if (options.create === false) throw new Error(`Missing Matterhorn Git epoch key for epoch ${epoch}; import it with matterhorn-git key import or set MATTERHORN_GIT_EPOCH_ROOT`);
|
|
100
|
+
ensurePrivateDir(path.dirname(file));
|
|
101
|
+
const root = randomBytes(32);
|
|
102
|
+
fs.writeFileSync(file, `${base64url(root)}\n`, { mode: 0o600 });
|
|
103
|
+
return root;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function importEpochRoot(originInput, epoch, secret, options = {}) {
|
|
107
|
+
const root = parseSecret(secret, "epoch root");
|
|
108
|
+
const file = epochRootFileFor(originInput, epoch, options);
|
|
109
|
+
ensurePrivateDir(path.dirname(file));
|
|
110
|
+
fs.writeFileSync(file, `${base64url(root)}\n`, { mode: 0o600 });
|
|
111
|
+
return { file, epoch: Number(epoch) || 1, secret: base64url(root) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function exportEpochRoot(originInput, epoch = 1, options = {}) {
|
|
115
|
+
const root = loadOrCreateEpochRoot(originInput, epoch, { ...options, create: false });
|
|
116
|
+
return base64url(root);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function chunkPlainIdFor(bytes, subkeys) {
|
|
120
|
+
return `hmac-sha256:${hmacSha256Hex(subkeys.chunkId, Buffer.from("matterhorn.git.chunk-id.v1\0"), bytes)}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function objectIdFor(kind, header, nonce, ciphertext, tag) {
|
|
124
|
+
return `sha256:${sha256Hex(Buffer.from(canonicalJson({ kind, ...header }), "utf8"), nonce, ciphertext, tag)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fileExists(file) {
|
|
128
|
+
let fd;
|
|
129
|
+
try {
|
|
130
|
+
fd = fs.openSync(file, "r");
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
} finally {
|
|
135
|
+
if (fd !== undefined) {
|
|
136
|
+
try { fs.closeSync(fd); } catch (matterhornIgnoredError) {
|
|
137
|
+
globalThis.__matterhornIgnoredError?.(matterhornIgnoredError, "packages/matterhorn-git/src/cas/storage.cjs");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writePrivateJson(file, value) {
|
|
144
|
+
ensurePrivateDir(path.dirname(file));
|
|
145
|
+
fs.writeFileSync(file, `${canonicalJson(value)}\n`, { mode: 0o600 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createChunkEnvelope({ origin, repoId, epoch, chunker, chunkPlainId, bytes, subkeys }) {
|
|
149
|
+
const aad = chunkAad({ repoId, epoch, chunkPlainId, clearLen: bytes.length, chunker });
|
|
150
|
+
const sealed = xChaCha20Poly1305Seal({ key: subkeys.chunkEnc, plaintext: bytes, aad });
|
|
151
|
+
const header = {
|
|
152
|
+
version: 1,
|
|
153
|
+
alg: "rk-git-chunks-v1",
|
|
154
|
+
aead: sealed.alg,
|
|
155
|
+
repoId,
|
|
156
|
+
roomName: origin.room,
|
|
157
|
+
keyEpoch: Number(epoch),
|
|
158
|
+
chunkPlainId,
|
|
159
|
+
clearLen: bytes.length,
|
|
160
|
+
chunker
|
|
161
|
+
};
|
|
162
|
+
const objectId = objectIdFor("matterhorn.git.chunk-object.v1", header, sealed.nonce, sealed.ciphertext, sealed.tag);
|
|
163
|
+
return {
|
|
164
|
+
kind: "matterhorn.git.chunk-object.v1",
|
|
165
|
+
...header,
|
|
166
|
+
objectId,
|
|
167
|
+
storedLen: sealed.ciphertext.length + sealed.tag.length,
|
|
168
|
+
nonce: base64url(sealed.nonce),
|
|
169
|
+
ciphertext: base64url(sealed.ciphertext),
|
|
170
|
+
tag: base64url(sealed.tag)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function decryptChunkEnvelope(envelope, originInput, options = {}) {
|
|
175
|
+
if (envelope.kind !== "matterhorn.git.chunk-object.v1") throw new Error("Not a Matterhorn Git chunk object");
|
|
176
|
+
const origin = parseMatterhornOrigin(originInput, { repo: envelope.repoId, room: envelope.roomName });
|
|
177
|
+
const epoch = Number(envelope.keyEpoch || envelope.epoch || 1);
|
|
178
|
+
const epochRoot = loadOrCreateEpochRoot(origin, epoch, { ...options, create: false });
|
|
179
|
+
const subkeys = deriveEpochSubkeys(epochRoot, { repoId: envelope.repoId || origin.repo, epoch });
|
|
180
|
+
const nonce = fromBase64url(envelope.nonce, "chunk nonce");
|
|
181
|
+
const ciphertext = fromBase64url(envelope.ciphertext, "chunk ciphertext");
|
|
182
|
+
const tag = fromBase64url(envelope.tag, "chunk tag");
|
|
183
|
+
const aad = chunkAad({ repoId: envelope.repoId || origin.repo, epoch, chunkPlainId: envelope.chunkPlainId, clearLen: envelope.clearLen, chunker: envelope.chunker });
|
|
184
|
+
const clear = xChaCha20Poly1305Open({ key: subkeys.chunkEnc, nonce, ciphertext, tag, aad });
|
|
185
|
+
const expectedId = objectIdFor("matterhorn.git.chunk-object.v1", {
|
|
186
|
+
version: envelope.version,
|
|
187
|
+
alg: envelope.alg,
|
|
188
|
+
aead: envelope.aead,
|
|
189
|
+
repoId: envelope.repoId,
|
|
190
|
+
roomName: envelope.roomName,
|
|
191
|
+
keyEpoch: envelope.keyEpoch,
|
|
192
|
+
chunkPlainId: envelope.chunkPlainId,
|
|
193
|
+
clearLen: envelope.clearLen,
|
|
194
|
+
chunker: envelope.chunker
|
|
195
|
+
}, nonce, ciphertext, tag);
|
|
196
|
+
if (envelope.objectId !== expectedId) throw new Error("Chunk object hash mismatch");
|
|
197
|
+
return clear;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function encryptManifest({ origin, repoId, epoch, manifestId, manifestPlaintext, subkeys }) {
|
|
201
|
+
const plaintext = Buffer.from(canonicalJson(manifestPlaintext), "utf8");
|
|
202
|
+
const aad = manifestAad({ repoId, epoch, manifestId });
|
|
203
|
+
const sealed = xChaCha20Poly1305Seal({ key: subkeys.manifestEnc, plaintext, aad });
|
|
204
|
+
const header = {
|
|
205
|
+
version: 1,
|
|
206
|
+
alg: "rk-git-chunks-v1",
|
|
207
|
+
aead: sealed.alg,
|
|
208
|
+
repoId,
|
|
209
|
+
roomName: origin.room,
|
|
210
|
+
keyEpoch: Number(epoch),
|
|
211
|
+
manifestId
|
|
212
|
+
};
|
|
213
|
+
const objectId = objectIdFor("matterhorn.git.encrypted-manifest.v1", header, sealed.nonce, sealed.ciphertext, sealed.tag);
|
|
214
|
+
return {
|
|
215
|
+
kind: "matterhorn.git.encrypted-manifest.v1",
|
|
216
|
+
...header,
|
|
217
|
+
objectId,
|
|
218
|
+
clearLen: plaintext.length,
|
|
219
|
+
storedLen: sealed.ciphertext.length + sealed.tag.length,
|
|
220
|
+
nonce: base64url(sealed.nonce),
|
|
221
|
+
ciphertext: base64url(sealed.ciphertext),
|
|
222
|
+
tag: base64url(sealed.tag)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function decryptManifestEnvelope(envelope, originInput, options = {}) {
|
|
227
|
+
if (envelope.kind !== "matterhorn.git.encrypted-manifest.v1") throw new Error("Not a Matterhorn Git encrypted manifest");
|
|
228
|
+
const origin = parseMatterhornOrigin(originInput, { repo: envelope.repoId, room: envelope.roomName });
|
|
229
|
+
const epoch = Number(envelope.keyEpoch || envelope.epoch || 1);
|
|
230
|
+
const epochRoot = loadOrCreateEpochRoot(origin, epoch, { ...options, create: false });
|
|
231
|
+
const subkeys = deriveEpochSubkeys(epochRoot, { repoId: envelope.repoId || origin.repo, epoch });
|
|
232
|
+
const nonce = fromBase64url(envelope.nonce, "manifest nonce");
|
|
233
|
+
const ciphertext = fromBase64url(envelope.ciphertext, "manifest ciphertext");
|
|
234
|
+
const tag = fromBase64url(envelope.tag, "manifest tag");
|
|
235
|
+
const aad = manifestAad({ repoId: envelope.repoId || origin.repo, epoch, manifestId: envelope.manifestId });
|
|
236
|
+
const plaintext = xChaCha20Poly1305Open({ key: subkeys.manifestEnc, nonce, ciphertext, tag, aad });
|
|
237
|
+
const expectedId = objectIdFor("matterhorn.git.encrypted-manifest.v1", {
|
|
238
|
+
version: envelope.version,
|
|
239
|
+
alg: envelope.alg,
|
|
240
|
+
aead: envelope.aead,
|
|
241
|
+
repoId: envelope.repoId,
|
|
242
|
+
roomName: envelope.roomName,
|
|
243
|
+
keyEpoch: envelope.keyEpoch,
|
|
244
|
+
manifestId: envelope.manifestId
|
|
245
|
+
}, nonce, ciphertext, tag);
|
|
246
|
+
if (envelope.objectId !== expectedId) throw new Error("Manifest object hash mismatch");
|
|
247
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function upsertIndexForChunk(index, chunkPlainId, object) {
|
|
251
|
+
const current = index.chunks[chunkPlainId] || { chunkPlainId, objects: [] };
|
|
252
|
+
const objects = current.objects.filter((item) => item.objectId !== object.objectId);
|
|
253
|
+
objects.push(object);
|
|
254
|
+
index.chunks[chunkPlainId] = { chunkPlainId, objects };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function findReusableChunk(index, originInput, epoch, chunkPlainId, options = {}) {
|
|
258
|
+
const entry = index.chunks?.[chunkPlainId];
|
|
259
|
+
if (!entry) return undefined;
|
|
260
|
+
return (entry.objects || []).find((object) => Number(object.epoch) === Number(epoch) && object.objectId && fileExists(objectPathFor(originInput, object.objectId, options)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function encryptBufferToCas(input, originInput, options = {}) {
|
|
264
|
+
const origin = parseMatterhornOrigin(originInput);
|
|
265
|
+
const repoId = origin.repo;
|
|
266
|
+
const epoch = Number(options.epoch || 1);
|
|
267
|
+
const fileMode = options.fileMode || "100644";
|
|
268
|
+
const profile = options.profile || DEFAULT_PROFILE;
|
|
269
|
+
const chunker = normalizeChunker(options.chunker || DEFAULT_CHUNKER);
|
|
270
|
+
const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input || "");
|
|
271
|
+
const epochRoot = loadOrCreateEpochRoot(origin, epoch, options);
|
|
272
|
+
const subkeys = deriveEpochSubkeys(epochRoot, { repoId, epoch });
|
|
273
|
+
const index = readCasIndex(origin, options);
|
|
274
|
+
const manifestChunks = [];
|
|
275
|
+
const createdObjects = [];
|
|
276
|
+
const allChunks = chunkBuffer(buffer, chunker);
|
|
277
|
+
|
|
278
|
+
for (const chunk of allChunks) {
|
|
279
|
+
const chunkPlainId = chunkPlainIdFor(chunk.bytes, subkeys);
|
|
280
|
+
const reusable = options.dedup === "none" ? undefined : findReusableChunk(index, origin, epoch, chunkPlainId, options);
|
|
281
|
+
let objectMeta;
|
|
282
|
+
if (reusable) {
|
|
283
|
+
objectMeta = { ...reusable, reused: true };
|
|
284
|
+
} else {
|
|
285
|
+
const envelope = createChunkEnvelope({ origin, repoId, epoch, chunker, chunkPlainId, bytes: chunk.bytes, subkeys });
|
|
286
|
+
writePrivateJson(objectPathFor(origin, envelope.objectId, options), envelope);
|
|
287
|
+
objectMeta = { objectId: envelope.objectId, epoch, clearLen: envelope.clearLen, storedLen: envelope.storedLen, createdAt: Date.now() };
|
|
288
|
+
upsertIndexForChunk(index, chunkPlainId, objectMeta);
|
|
289
|
+
index.objects[envelope.objectId] = { kind: envelope.kind, epoch, chunkPlainId, clearLen: envelope.clearLen, storedLen: envelope.storedLen, path: objectPathFor(origin, envelope.objectId, options), createdAt: objectMeta.createdAt };
|
|
290
|
+
createdObjects.push(envelope.objectId);
|
|
291
|
+
}
|
|
292
|
+
manifestChunks.push({
|
|
293
|
+
index: chunk.index,
|
|
294
|
+
chunkPlainId,
|
|
295
|
+
clearLen: chunk.clearLen,
|
|
296
|
+
objectCandidates: [objectMeta.objectId]
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const manifestId = createLogicalManifestIdentity({ repoId, epoch, fileMode, clearSize: buffer.length, chunker, chunks: manifestChunks }, subkeys.manifestId);
|
|
301
|
+
const manifestPlaintext = {
|
|
302
|
+
kind: "matterhorn.git.file-manifest.v1",
|
|
303
|
+
version: 1,
|
|
304
|
+
repoId,
|
|
305
|
+
roomName: origin.room,
|
|
306
|
+
keyEpoch: epoch,
|
|
307
|
+
file: { mode: fileMode, clearSize: buffer.length },
|
|
308
|
+
profile,
|
|
309
|
+
alg: {
|
|
310
|
+
chunker,
|
|
311
|
+
chunkId: "hmac-sha256",
|
|
312
|
+
objectHash: "sha256",
|
|
313
|
+
aead: "xchacha20-poly1305-ietf",
|
|
314
|
+
compression: "none"
|
|
315
|
+
},
|
|
316
|
+
chunks: manifestChunks,
|
|
317
|
+
logicalHash: `sha256:${sha256Hex(buffer)}`
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
let manifestObjectId;
|
|
321
|
+
const knownManifest = index.manifests[manifestId];
|
|
322
|
+
if (knownManifest && fileExists(manifestPathFor(origin, knownManifest.objectId, options))) {
|
|
323
|
+
manifestObjectId = knownManifest.objectId;
|
|
324
|
+
} else {
|
|
325
|
+
const manifestEnvelope = encryptManifest({ origin, repoId, epoch, manifestId, manifestPlaintext, subkeys });
|
|
326
|
+
writePrivateJson(manifestPathFor(origin, manifestEnvelope.objectId, options), manifestEnvelope);
|
|
327
|
+
manifestObjectId = manifestEnvelope.objectId;
|
|
328
|
+
index.manifestObjects[manifestObjectId] = { kind: manifestEnvelope.kind, epoch, manifestId, clearLen: manifestEnvelope.clearLen, storedLen: manifestEnvelope.storedLen, path: manifestPathFor(origin, manifestObjectId, options), createdAt: Date.now() };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
index.manifests[manifestId] = {
|
|
332
|
+
manifestId,
|
|
333
|
+
objectId: manifestObjectId,
|
|
334
|
+
repoId,
|
|
335
|
+
epoch,
|
|
336
|
+
clearSize: buffer.length,
|
|
337
|
+
chunkCount: manifestChunks.length,
|
|
338
|
+
pointerProfile: profile,
|
|
339
|
+
updatedAt: Date.now(),
|
|
340
|
+
chunks: manifestChunks.map((chunk) => ({ index: chunk.index, chunkPlainId: chunk.chunkPlainId, clearLen: chunk.clearLen, objectCandidates: chunk.objectCandidates }))
|
|
341
|
+
};
|
|
342
|
+
writeCasIndex(origin, index, options);
|
|
343
|
+
|
|
344
|
+
const pointer = createPointer({ repoId, epoch, manifestId, clearSize: buffer.length, chunkCount: manifestChunks.length, profile });
|
|
345
|
+
return {
|
|
346
|
+
origin,
|
|
347
|
+
repoId,
|
|
348
|
+
epoch,
|
|
349
|
+
pointer,
|
|
350
|
+
pointerFields: parsePointer(pointer),
|
|
351
|
+
manifestId,
|
|
352
|
+
manifestObjectId,
|
|
353
|
+
clearSize: buffer.length,
|
|
354
|
+
chunkCount: manifestChunks.length,
|
|
355
|
+
chunks: manifestChunks,
|
|
356
|
+
createdObjects,
|
|
357
|
+
profile
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = {
|
|
362
|
+
DEFAULT_CHUNKER,
|
|
363
|
+
DEFAULT_PROFILE,
|
|
364
|
+
ensurePrivateDir,
|
|
365
|
+
safeId,
|
|
366
|
+
casRootFor,
|
|
367
|
+
keyRootFor,
|
|
368
|
+
indexFileFor,
|
|
369
|
+
objectPathFor,
|
|
370
|
+
manifestPathFor,
|
|
371
|
+
epochRootFileFor,
|
|
372
|
+
readJson,
|
|
373
|
+
readCasIndex,
|
|
374
|
+
writeCasIndex,
|
|
375
|
+
envEpochRoot,
|
|
376
|
+
loadOrCreateEpochRoot,
|
|
377
|
+
importEpochRoot,
|
|
378
|
+
exportEpochRoot,
|
|
379
|
+
chunkPlainIdFor,
|
|
380
|
+
fileExists,
|
|
381
|
+
writePrivateJson,
|
|
382
|
+
createChunkEnvelope,
|
|
383
|
+
decryptChunkEnvelope,
|
|
384
|
+
encryptManifest,
|
|
385
|
+
decryptManifestEnvelope,
|
|
386
|
+
upsertIndexForChunk,
|
|
387
|
+
findReusableChunk,
|
|
388
|
+
encryptBufferToCas
|
|
389
|
+
};
|
package/src/cas.cjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const storage = require("./cas/storage.cjs");
|
|
2
|
+
const pointer = require("./cas/pointer.cjs");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
DEFAULT_CHUNKER: storage.DEFAULT_CHUNKER,
|
|
6
|
+
DEFAULT_PROFILE: storage.DEFAULT_PROFILE,
|
|
7
|
+
casRootFor: storage.casRootFor,
|
|
8
|
+
chunkPlainIdFor: storage.chunkPlainIdFor,
|
|
9
|
+
decryptChunkEnvelope: storage.decryptChunkEnvelope,
|
|
10
|
+
decryptManifestEnvelope: storage.decryptManifestEnvelope,
|
|
11
|
+
encryptBufferToCas: storage.encryptBufferToCas,
|
|
12
|
+
epochRootFileFor: storage.epochRootFileFor,
|
|
13
|
+
exportEpochRoot: storage.exportEpochRoot,
|
|
14
|
+
importEpochRoot: storage.importEpochRoot,
|
|
15
|
+
indexFileFor: storage.indexFileFor,
|
|
16
|
+
isMatterhornPointer: pointer.isMatterhornPointer,
|
|
17
|
+
loadOrCreateEpochRoot: storage.loadOrCreateEpochRoot,
|
|
18
|
+
manifestEnvelopeForPointer: pointer.manifestEnvelopeForPointer,
|
|
19
|
+
manifestPathFor: storage.manifestPathFor,
|
|
20
|
+
materializeCasObjectOutbox: pointer.materializeCasObjectOutbox,
|
|
21
|
+
metadataForPointer: pointer.metadataForPointer,
|
|
22
|
+
objectPathFor: storage.objectPathFor,
|
|
23
|
+
readCasIndex: storage.readCasIndex,
|
|
24
|
+
restorePointerFromCas: pointer.restorePointerFromCas,
|
|
25
|
+
writeCasIndex: storage.writeCasIndex
|
|
26
|
+
};
|
package/src/chunker.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CHUNKER = Object.freeze({
|
|
4
|
+
name: "fastcdc-js",
|
|
5
|
+
version: "v1",
|
|
6
|
+
min: 4 * 1024,
|
|
7
|
+
target: 16 * 1024,
|
|
8
|
+
max: 64 * 1024
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
let gearTable;
|
|
12
|
+
|
|
13
|
+
function normalizeChunker(input = {}) {
|
|
14
|
+
const out = {
|
|
15
|
+
name: input.name || DEFAULT_CHUNKER.name,
|
|
16
|
+
version: input.version || DEFAULT_CHUNKER.version,
|
|
17
|
+
min: Number(input.min || DEFAULT_CHUNKER.min),
|
|
18
|
+
target: Number(input.target || DEFAULT_CHUNKER.target),
|
|
19
|
+
max: Number(input.max || DEFAULT_CHUNKER.max)
|
|
20
|
+
};
|
|
21
|
+
if (!Number.isInteger(out.min) || out.min < 128) throw new Error("chunker.min must be an integer >= 128");
|
|
22
|
+
if (!Number.isInteger(out.target) || out.target < out.min) throw new Error("chunker.target must be an integer >= min");
|
|
23
|
+
if (!Number.isInteger(out.max) || out.max < out.target) throw new Error("chunker.max must be an integer >= target");
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function gear() {
|
|
28
|
+
if (gearTable) return gearTable;
|
|
29
|
+
gearTable = Array.from({ length: 256 }, (_, i) => {
|
|
30
|
+
const digest = crypto.createHash("sha256").update(`matterhorn.fastcdc-js.v1.gear.${i}`).digest();
|
|
31
|
+
return digest.readBigUInt64LE(0);
|
|
32
|
+
});
|
|
33
|
+
return gearTable;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function maskForTarget(target) {
|
|
37
|
+
const bits = Math.max(8, Math.round(Math.log2(target)));
|
|
38
|
+
return (1n << BigInt(bits)) - 1n;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function chunkBuffer(input, chunkerInput = {}) {
|
|
42
|
+
const buffer = Buffer.isBuffer(input) ? input : Buffer.from(input || "");
|
|
43
|
+
const chunker = normalizeChunker(chunkerInput);
|
|
44
|
+
if (buffer.length === 0) return [];
|
|
45
|
+
const table = gear();
|
|
46
|
+
const mask = maskForTarget(chunker.target);
|
|
47
|
+
const chunks = [];
|
|
48
|
+
let start = 0;
|
|
49
|
+
while (start < buffer.length) {
|
|
50
|
+
let fp = 0n;
|
|
51
|
+
let end = Math.min(buffer.length, start + chunker.max);
|
|
52
|
+
for (let pos = start; pos < end; pos += 1) {
|
|
53
|
+
fp = ((fp << 1n) + table[buffer[pos]]) & 0xffffffffffffffffn;
|
|
54
|
+
const len = pos - start + 1;
|
|
55
|
+
if (len >= chunker.min && ((fp & mask) === 0n || len >= chunker.max)) {
|
|
56
|
+
end = pos + 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const bytes = buffer.subarray(start, end);
|
|
61
|
+
chunks.push({ index: chunks.length, offset: start, clearLen: bytes.length, bytes });
|
|
62
|
+
start = end;
|
|
63
|
+
}
|
|
64
|
+
return chunks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { DEFAULT_CHUNKER, chunkBuffer, normalizeChunker };
|