@holoscript/holosystem 0.2.2 → 0.2.4

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.
@@ -0,0 +1,979 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import {
4
+ HOLOSYSTEM_SUBSTRATE_IMPORT_SCHEMA,
5
+ _substrateImportInternals,
6
+ } from './substrate-import.mjs';
7
+ import { verifyDebianRepositoryRelease } from './substrate-debian-release.mjs';
8
+
9
+ const {
10
+ compareText,
11
+ hashJson,
12
+ hashReceipt,
13
+ isRecord,
14
+ issue,
15
+ pinnedRevision,
16
+ portableSource,
17
+ sanitizeVerificationPolicy,
18
+ validId,
19
+ } = _substrateImportInternals;
20
+
21
+ const MAX_CONTROL_BYTES = 128 * 1024 * 1024;
22
+ const PACKAGE_NAME_PATTERN = /^[a-z0-9][a-z0-9+.-]{1,127}$/u;
23
+ const ARCHITECTURE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
24
+ const FIELD_NAME_PATTERN = /^[!"$-),-9;-~]+$/u;
25
+ const DIGEST_PATTERN = /^(?:sha256:[a-f0-9]{64}|sha512:[a-f0-9]{128})$/u;
26
+ const MAINTAINER_SCRIPT_NAMES = new Set(['config', 'postinst', 'postrm', 'preinst', 'prerm']);
27
+
28
+ function validPackageName(value) {
29
+ return typeof value === 'string' && PACKAGE_NAME_PATTERN.test(value);
30
+ }
31
+
32
+ function validArchitecture(value) {
33
+ return typeof value === 'string' && ARCHITECTURE_PATTERN.test(value);
34
+ }
35
+
36
+ function validDebianVersion(value) {
37
+ if (typeof value !== 'string' || value.length === 0 || value.length > 160) return false;
38
+ const colon = value.indexOf(':');
39
+ const hasEpoch = colon !== -1;
40
+ if (hasEpoch && (!/^\d+$/u.test(value.slice(0, colon)) || value.indexOf(':', colon + 1) !== -1)) {
41
+ return false;
42
+ }
43
+ const remainder = hasEpoch ? value.slice(colon + 1) : value;
44
+ const hyphen = remainder.lastIndexOf('-');
45
+ const upstream = hyphen === -1 ? remainder : remainder.slice(0, hyphen);
46
+ const revision = hyphen === -1 ? null : remainder.slice(hyphen + 1);
47
+ return (
48
+ /^[0-9][A-Za-z0-9.+~:-]*$/u.test(upstream) &&
49
+ (hasEpoch || !upstream.includes(':')) &&
50
+ (revision == null || /^[A-Za-z0-9+.~]+$/u.test(revision))
51
+ );
52
+ }
53
+
54
+ function validDigest(value) {
55
+ return typeof value === 'string' && DIGEST_PATTERN.test(value);
56
+ }
57
+
58
+ function digestText(value, algorithm) {
59
+ return `${algorithm}:${createHash(algorithm).update(value, 'utf8').digest('hex')}`;
60
+ }
61
+
62
+ function verifyTextDigest(value, expected) {
63
+ if (!validDigest(expected)) return false;
64
+ const separator = expected.indexOf(':');
65
+ return digestText(value, expected.slice(0, separator)) === expected;
66
+ }
67
+
68
+ function parseControl(text, label, issues) {
69
+ if (typeof text !== 'string') {
70
+ issue(issues, 'control-input-invalid', label, 'Debian control input must be UTF-8 text.');
71
+ return [];
72
+ }
73
+ if (Buffer.byteLength(text, 'utf8') > MAX_CONTROL_BYTES) {
74
+ issue(issues, 'control-input-too-large', label, 'Debian control input exceeds 128 MiB.');
75
+ return [];
76
+ }
77
+ if (text.includes('\0')) {
78
+ issue(issues, 'control-input-nul', label, 'Debian control input must not contain NUL bytes.');
79
+ return [];
80
+ }
81
+
82
+ const paragraphs = [];
83
+ let paragraph = {};
84
+ let currentField = null;
85
+ const flush = () => {
86
+ if (Object.keys(paragraph).length > 0) paragraphs.push(paragraph);
87
+ paragraph = {};
88
+ currentField = null;
89
+ };
90
+
91
+ for (const [lineIndex, rawLine] of text.replaceAll('\r\n', '\n').split('\n').entries()) {
92
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
93
+ if (/^[\t ]*$/u.test(line)) {
94
+ flush();
95
+ continue;
96
+ }
97
+ if (/^[\t ]/u.test(line)) {
98
+ if (!currentField) {
99
+ issue(
100
+ issues,
101
+ 'control-continuation-orphan',
102
+ `${label}.line-${lineIndex + 1}`,
103
+ 'Continuation line has no preceding field.'
104
+ );
105
+ } else {
106
+ paragraph[currentField] += `\n${line.slice(1)}`;
107
+ }
108
+ continue;
109
+ }
110
+
111
+ const separator = line.indexOf(':');
112
+ const fieldName = separator === -1 ? '' : line.slice(0, separator);
113
+ if (
114
+ separator <= 0 ||
115
+ fieldName.startsWith('#') ||
116
+ fieldName.startsWith('-') ||
117
+ !FIELD_NAME_PATTERN.test(fieldName)
118
+ ) {
119
+ issue(
120
+ issues,
121
+ 'control-field-malformed',
122
+ `${label}.line-${lineIndex + 1}`,
123
+ 'Control line must contain a valid field name followed by a colon.'
124
+ );
125
+ currentField = null;
126
+ continue;
127
+ }
128
+ currentField = fieldName.toLowerCase();
129
+ if (Object.hasOwn(paragraph, currentField)) {
130
+ issue(
131
+ issues,
132
+ 'control-field-duplicate',
133
+ `${label}.line-${lineIndex + 1}`,
134
+ 'Control stanza contains a duplicate field.'
135
+ );
136
+ currentField = null;
137
+ continue;
138
+ }
139
+ paragraph[currentField] = line.slice(separator + 1).trim();
140
+ }
141
+ flush();
142
+ return paragraphs;
143
+ }
144
+
145
+ function packageIdentity(name, architecture) {
146
+ return `${name}:${architecture}`;
147
+ }
148
+
149
+ function componentId(identity) {
150
+ const readable = identity
151
+ .toLowerCase()
152
+ .replace(/[^a-z0-9._-]+/gu, '-')
153
+ .replace(/^-+|-+$/gu, '')
154
+ .slice(0, 80);
155
+ const suffix = createHash('sha256').update(identity).digest('hex').slice(0, 12);
156
+ return `deb-${readable || 'package'}-${suffix}`;
157
+ }
158
+
159
+ function missingComponentId(fromIdentity, relation) {
160
+ const suffix = createHash('sha256')
161
+ .update(`${fromIdentity}\0${relation}`)
162
+ .digest('hex')
163
+ .slice(0, 12);
164
+ return `deb-missing-${suffix}`;
165
+ }
166
+
167
+ function normalizeInstalledPackages(paragraphs, issues) {
168
+ const installed = [];
169
+ const identities = new Set();
170
+ for (const [index, fields] of paragraphs.entries()) {
171
+ const status = String(fields.status || '')
172
+ .trim()
173
+ .split(/\s+/u);
174
+ if (status.length !== 3 || status[1] !== 'ok' || status[2] !== 'installed') continue;
175
+ const path = `status.stanzas[${index}]`;
176
+ const name = validPackageName(fields.package) ? fields.package : null;
177
+ const architecture = validArchitecture(fields.architecture) ? fields.architecture : null;
178
+ const version = validDebianVersion(fields.version) ? fields.version : null;
179
+ if (!name) {
180
+ issue(
181
+ issues,
182
+ 'installed-package-name-invalid',
183
+ `${path}.Package`,
184
+ 'Installed package name is invalid.'
185
+ );
186
+ }
187
+ if (!architecture) {
188
+ issue(
189
+ issues,
190
+ 'installed-package-architecture-invalid',
191
+ `${path}.Architecture`,
192
+ 'Installed package architecture is invalid.'
193
+ );
194
+ }
195
+ if (!version) {
196
+ issue(
197
+ issues,
198
+ 'installed-package-version-invalid',
199
+ `${path}.Version`,
200
+ 'Installed package version is not an exact Debian version.'
201
+ );
202
+ }
203
+ const identity = name && architecture ? packageIdentity(name, architecture) : null;
204
+ if (identity && identities.has(identity)) {
205
+ issue(
206
+ issues,
207
+ 'installed-package-duplicate',
208
+ path,
209
+ 'Installed package identity occurs more than once.'
210
+ );
211
+ }
212
+ if (identity) identities.add(identity);
213
+ installed.push({
214
+ architecture,
215
+ depends: fields.depends || '',
216
+ fields,
217
+ id: identity ? componentId(identity) : null,
218
+ identity,
219
+ multiArch: fields['multi-arch'] || null,
220
+ name,
221
+ preDepends: fields['pre-depends'] || '',
222
+ provides: fields.provides || '',
223
+ version,
224
+ });
225
+ }
226
+ installed.sort((left, right) => compareText(left.identity, right.identity));
227
+ if (installed.length === 0) {
228
+ issue(
229
+ issues,
230
+ 'installed-packages-missing',
231
+ 'status',
232
+ 'Status input contains no fully installed packages.'
233
+ );
234
+ }
235
+ return installed;
236
+ }
237
+
238
+ function normalizeCustody(value, path, issues) {
239
+ const owner = validId(value?.owner) ? value.owner : null;
240
+ const trustDomain = validId(value?.trustDomain) ? value.trustDomain : null;
241
+ if (!owner || !trustDomain) {
242
+ issue(
243
+ issues,
244
+ 'external-custody-invalid',
245
+ path,
246
+ 'External Debian archive custody owner and trust domain must be portable identifiers.'
247
+ );
248
+ }
249
+ return { mode: 'external', owner, trustDomain };
250
+ }
251
+
252
+ function normalizeRepository(repository, packagesIndex, path, issues, now) {
253
+ let uri = null;
254
+ if (portableSource(repository?.uri) && String(repository.uri).startsWith('https://')) {
255
+ const parsed = new URL(repository.uri);
256
+ parsed.pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
257
+ uri = parsed.toString();
258
+ } else {
259
+ issue(
260
+ issues,
261
+ 'repository-uri-invalid',
262
+ `${path}.uri`,
263
+ 'Repository must be a credential-free HTTPS base URI.'
264
+ );
265
+ }
266
+ const packagesIndexDigest = validDigest(repository?.packagesIndexDigest)
267
+ ? repository.packagesIndexDigest
268
+ : null;
269
+ if (!packagesIndexDigest) {
270
+ issue(
271
+ issues,
272
+ 'packages-index-digest-invalid',
273
+ `${path}.packagesIndexDigest`,
274
+ 'Packages index requires a sha256 or sha512 caller anchor.'
275
+ );
276
+ } else if (
277
+ typeof packagesIndex === 'string' &&
278
+ !verifyTextDigest(packagesIndex, packagesIndexDigest)
279
+ ) {
280
+ issue(
281
+ issues,
282
+ 'packages-index-digest-mismatch',
283
+ `${path}.packagesIndex`,
284
+ 'Packages index does not match its caller-provided digest.'
285
+ );
286
+ }
287
+ const authentication = repository?.authentication
288
+ ? verifyDebianRepositoryRelease({
289
+ ...repository.authentication,
290
+ packagesIndex,
291
+ packagesIndexDigest,
292
+ now,
293
+ })
294
+ : null;
295
+ for (const finding of authentication?.issues || []) {
296
+ issue(issues, finding.code, `${path}.authentication.${finding.path}`, finding.message);
297
+ }
298
+ return { authentication, packagesIndexDigest, uri };
299
+ }
300
+
301
+ function portableFilename(value) {
302
+ return (
303
+ typeof value === 'string' &&
304
+ value.length > 0 &&
305
+ value.length <= 512 &&
306
+ !value.startsWith('/') &&
307
+ !value.includes('\\') &&
308
+ !value.includes('%') &&
309
+ !value.includes('?') &&
310
+ !value.includes('#') &&
311
+ !value.split('/').includes('..')
312
+ );
313
+ }
314
+
315
+ function artifactDigest(fields) {
316
+ if (typeof fields.sha512 === 'string' && /^[a-f0-9]{128}$/u.test(fields.sha512)) {
317
+ return `sha512:${fields.sha512}`;
318
+ }
319
+ if (typeof fields.sha256 === 'string' && /^[a-f0-9]{64}$/u.test(fields.sha256)) {
320
+ return `sha256:${fields.sha256}`;
321
+ }
322
+ return null;
323
+ }
324
+
325
+ function buildPackageIndex(paragraphs, repository, custody, wantedKeys, path, issues) {
326
+ const entries = new Map();
327
+ for (const [index, fields] of paragraphs.entries()) {
328
+ const stanzaPath = `${path}.packagesIndex.stanzas[${index}]`;
329
+ const name = validPackageName(fields.package) ? fields.package : null;
330
+ const architecture = validArchitecture(fields.architecture) ? fields.architecture : null;
331
+ const version = validDebianVersion(fields.version) ? fields.version : null;
332
+ if (!name || !architecture || !version) continue;
333
+ const key = `${packageIdentity(name, architecture)}@${version}`;
334
+ if (!wantedKeys.has(key)) continue;
335
+ const digest = artifactDigest(fields);
336
+ const filename = portableFilename(fields.filename) ? fields.filename : null;
337
+ let sourceUri = null;
338
+ if (repository.uri && filename) {
339
+ const candidate = new URL(filename, repository.uri);
340
+ sourceUri = candidate.origin === new URL(repository.uri).origin ? candidate.toString() : null;
341
+ }
342
+ if (!digest) {
343
+ issue(
344
+ issues,
345
+ 'repository-package-digest-invalid',
346
+ `${stanzaPath}.SHA256`,
347
+ 'Repository package requires a canonical SHA-256 or SHA-512 digest.'
348
+ );
349
+ }
350
+ if (!filename || !sourceUri) {
351
+ issue(
352
+ issues,
353
+ 'repository-package-filename-invalid',
354
+ `${stanzaPath}.Filename`,
355
+ 'Repository package filename must remain below the configured HTTPS base.'
356
+ );
357
+ }
358
+ if (entries.has(key)) {
359
+ issue(
360
+ issues,
361
+ 'repository-package-duplicate',
362
+ stanzaPath,
363
+ 'Packages index contains a duplicate package, architecture, and version tuple.'
364
+ );
365
+ continue;
366
+ }
367
+ entries.set(key, {
368
+ custody,
369
+ digest,
370
+ filename,
371
+ packagesIndexDigest: repository.packagesIndexDigest,
372
+ repositoryUri: repository.uri,
373
+ sourceUri,
374
+ });
375
+ }
376
+ return entries;
377
+ }
378
+
379
+ function buildRepositorySources(
380
+ { sources, packagesIndex, repository, externalCustody },
381
+ installed,
382
+ issues,
383
+ now
384
+ ) {
385
+ const fallbackCustody = normalizeCustody(externalCustody, 'externalCustody', issues);
386
+ const hasSources = sources != null;
387
+ if (hasSources && (packagesIndex != null || repository != null)) {
388
+ issue(
389
+ issues,
390
+ 'repository-sources-conflict',
391
+ 'sources',
392
+ 'Use either sources or the single packagesIndex and repository inputs, not both.'
393
+ );
394
+ }
395
+ const rawSources = hasSources ? sources : [{ packagesIndex, repository }];
396
+ if (!Array.isArray(rawSources) || rawSources.length === 0) {
397
+ issue(
398
+ issues,
399
+ 'repository-sources-missing',
400
+ 'sources',
401
+ 'At least one digest-anchored repository source is required.'
402
+ );
403
+ return { evidence: [], packageIndex: new Map() };
404
+ }
405
+
406
+ const normalizedSources = rawSources.map((source, index) => {
407
+ const path = hasSources ? `sources[${index}]` : 'repository';
408
+ const text = source?.packagesIndex;
409
+ const normalizedRepository = normalizeRepository(source?.repository, text, path, issues, now);
410
+ const custody = source?.custody
411
+ ? normalizeCustody(source.custody, `${path}.custody`, issues)
412
+ : fallbackCustody;
413
+ return {
414
+ custody,
415
+ index,
416
+ paragraphs: parseControl(text, `${path}.packagesIndex`, issues),
417
+ path,
418
+ repository: normalizedRepository,
419
+ };
420
+ });
421
+ normalizedSources.sort(
422
+ (left, right) =>
423
+ compareText(left.repository.uri, right.repository.uri) ||
424
+ compareText(left.repository.packagesIndexDigest, right.repository.packagesIndexDigest)
425
+ );
426
+
427
+ const packageIndex = new Map();
428
+ const wantedKeys = new Set(installed.map((item) => `${item.identity}@${item.version}`));
429
+ for (const source of normalizedSources) {
430
+ const entries = buildPackageIndex(
431
+ source.paragraphs,
432
+ source.repository,
433
+ source.custody,
434
+ wantedKeys,
435
+ source.path,
436
+ issues
437
+ );
438
+ for (const [key, entry] of entries) {
439
+ if (packageIndex.has(key)) {
440
+ issue(
441
+ issues,
442
+ 'repository-package-source-ambiguous',
443
+ `${source.path}.packagesIndex`,
444
+ 'Package, architecture, and version tuple occurs in more than one repository source.'
445
+ );
446
+ continue;
447
+ }
448
+ packageIndex.set(key, entry);
449
+ }
450
+ }
451
+
452
+ return {
453
+ evidence: normalizedSources.map((source) => ({
454
+ authentication: source.repository.authentication,
455
+ custody: source.custody,
456
+ packagesIndexDigest: source.repository.packagesIndexDigest,
457
+ uri: source.repository.uri,
458
+ })),
459
+ allAuthenticated: normalizedSources.every(
460
+ (source) => source.repository.authentication?.verified === true
461
+ ),
462
+ packageIndex,
463
+ };
464
+ }
465
+
466
+ function normalizeMaintainerScripts(value, installed, issues) {
467
+ if (!isRecord(value)) {
468
+ issue(
469
+ issues,
470
+ 'maintainer-script-manifest-invalid',
471
+ 'maintainerScripts',
472
+ 'Maintainer script manifest must map every installed package identity to script digests.'
473
+ );
474
+ }
475
+ const manifest = {};
476
+ const installedIdentities = new Set(installed.map((item) => item.identity).filter(Boolean));
477
+ for (const identity of [...installedIdentities].sort(compareText)) {
478
+ const scripts = value?.[identity];
479
+ if (!isRecord(scripts)) {
480
+ issue(
481
+ issues,
482
+ 'maintainer-script-package-missing',
483
+ `maintainerScripts.${identity}`,
484
+ 'Every installed package needs an explicit maintainer script map; use an empty object for none.'
485
+ );
486
+ manifest[identity] = {};
487
+ continue;
488
+ }
489
+ manifest[identity] = {};
490
+ for (const name of Object.keys(scripts).sort(compareText)) {
491
+ if (!MAINTAINER_SCRIPT_NAMES.has(name)) {
492
+ issue(
493
+ issues,
494
+ 'maintainer-script-name-invalid',
495
+ `maintainerScripts.${identity}.${name}`,
496
+ 'Maintainer script name must be preinst, postinst, prerm, postrm, or config.'
497
+ );
498
+ continue;
499
+ }
500
+ if (!validDigest(scripts[name])) {
501
+ issue(
502
+ issues,
503
+ 'maintainer-script-digest-invalid',
504
+ `maintainerScripts.${identity}.${name}`,
505
+ 'Maintainer script digest must be sha256 or sha512.'
506
+ );
507
+ continue;
508
+ }
509
+ manifest[identity][name] = scripts[name];
510
+ }
511
+ }
512
+ for (const identity of Object.keys(isRecord(value) ? value : {}).sort(compareText)) {
513
+ if (!installedIdentities.has(identity)) {
514
+ issue(
515
+ issues,
516
+ 'maintainer-script-package-unknown',
517
+ `maintainerScripts.${identity}`,
518
+ 'Maintainer script manifest names a package that is not installed.'
519
+ );
520
+ }
521
+ }
522
+ return manifest;
523
+ }
524
+
525
+ function splitDebianVersion(version) {
526
+ const colon = version.indexOf(':');
527
+ const epoch = colon === -1 ? 0n : BigInt(version.slice(0, colon));
528
+ const remainder = colon === -1 ? version : version.slice(colon + 1);
529
+ const hyphen = remainder.lastIndexOf('-');
530
+ return {
531
+ epoch,
532
+ revision: hyphen === -1 ? null : remainder.slice(hyphen + 1),
533
+ upstream: hyphen === -1 ? remainder : remainder.slice(0, hyphen),
534
+ };
535
+ }
536
+
537
+ function orderCharacter(value) {
538
+ if (!value || /[0-9]/u.test(value)) return 0;
539
+ if (value === '~') return -1;
540
+ if (/[A-Za-z]/u.test(value)) return value.codePointAt(0);
541
+ return value.codePointAt(0) + 256;
542
+ }
543
+
544
+ function compareVersionPart(left, right) {
545
+ let leftIndex = 0;
546
+ let rightIndex = 0;
547
+ while (leftIndex < left.length || rightIndex < right.length) {
548
+ while (
549
+ (leftIndex < left.length && !/[0-9]/u.test(left[leftIndex])) ||
550
+ (rightIndex < right.length && !/[0-9]/u.test(right[rightIndex]))
551
+ ) {
552
+ const leftOrder = orderCharacter(left[leftIndex]);
553
+ const rightOrder = orderCharacter(right[rightIndex]);
554
+ if (leftOrder !== rightOrder) return leftOrder < rightOrder ? -1 : 1;
555
+ if (leftIndex < left.length && !/[0-9]/u.test(left[leftIndex])) leftIndex += 1;
556
+ if (rightIndex < right.length && !/[0-9]/u.test(right[rightIndex])) rightIndex += 1;
557
+ }
558
+
559
+ while (left[leftIndex] === '0') leftIndex += 1;
560
+ while (right[rightIndex] === '0') rightIndex += 1;
561
+ let leftEnd = leftIndex;
562
+ let rightEnd = rightIndex;
563
+ while (/[0-9]/u.test(left[leftEnd] || '')) leftEnd += 1;
564
+ while (/[0-9]/u.test(right[rightEnd] || '')) rightEnd += 1;
565
+ const leftLength = leftEnd - leftIndex;
566
+ const rightLength = rightEnd - rightIndex;
567
+ if (leftLength !== rightLength) return leftLength < rightLength ? -1 : 1;
568
+ const leftDigits = left.slice(leftIndex, leftEnd);
569
+ const rightDigits = right.slice(rightIndex, rightEnd);
570
+ if (leftDigits !== rightDigits) return leftDigits < rightDigits ? -1 : 1;
571
+ leftIndex = leftEnd;
572
+ rightIndex = rightEnd;
573
+ }
574
+ return 0;
575
+ }
576
+
577
+ function compareDebianVersions(left, right) {
578
+ const leftParts = splitDebianVersion(left);
579
+ const rightParts = splitDebianVersion(right);
580
+ if (leftParts.epoch !== rightParts.epoch) return leftParts.epoch < rightParts.epoch ? -1 : 1;
581
+ const upstream = compareVersionPart(leftParts.upstream, rightParts.upstream);
582
+ if (upstream !== 0) return upstream;
583
+ if (leftParts.revision == null || rightParts.revision == null) {
584
+ if (leftParts.revision == null && rightParts.revision == null) return 0;
585
+ return leftParts.revision == null ? -1 : 1;
586
+ }
587
+ return compareVersionPart(leftParts.revision, rightParts.revision);
588
+ }
589
+
590
+ export const _debianImportInternals = Object.freeze({ compareDebianVersions });
591
+
592
+ function versionSatisfies(version, relation, expected) {
593
+ if (!relation) return true;
594
+ const compared = compareDebianVersions(version, expected);
595
+ return {
596
+ '<<': compared < 0,
597
+ '<=': compared <= 0,
598
+ '=': compared === 0,
599
+ '>=': compared >= 0,
600
+ '>>': compared > 0,
601
+ }[relation];
602
+ }
603
+
604
+ function parseRelationAtom(value) {
605
+ const match = String(value)
606
+ .trim()
607
+ .match(
608
+ /^([a-z0-9][a-z0-9+.-]*)(?::([a-z0-9-]+))?(?:\s*\(\s*(<<|<=|=|>=|>>)\s*([^\s)]+)\s*\))?$/u
609
+ );
610
+ if (!match || !validPackageName(match[1]) || (match[4] && !validDebianVersion(match[4]))) {
611
+ return null;
612
+ }
613
+ return {
614
+ name: match[1],
615
+ qualifier: match[2] || null,
616
+ relation: match[3] || null,
617
+ version: match[4] || null,
618
+ };
619
+ }
620
+
621
+ function parseProvides(installed, issues) {
622
+ const providers = new Map();
623
+ for (const item of installed) {
624
+ if (!item.provides) continue;
625
+ for (const raw of item.provides.replaceAll('\n', ' ').split(',')) {
626
+ const atom = parseRelationAtom(raw);
627
+ if (!atom || atom.qualifier || (atom.relation && atom.relation !== '=')) {
628
+ issue(
629
+ issues,
630
+ 'provides-relation-invalid',
631
+ `status.${item.identity}.Provides`,
632
+ 'Provides entries must be package names with an optional exact version.'
633
+ );
634
+ continue;
635
+ }
636
+ const values = providers.get(atom.name) || [];
637
+ values.push({ item, version: atom.version });
638
+ providers.set(atom.name, values);
639
+ }
640
+ }
641
+ return providers;
642
+ }
643
+
644
+ function architectureMatches(candidate, qualifier, dependentArchitecture) {
645
+ if (qualifier === 'any') return candidate.multiArch === 'allowed';
646
+ if (qualifier && qualifier !== 'native') return candidate.architecture === qualifier;
647
+ return (
648
+ candidate.architecture === dependentArchitecture ||
649
+ candidate.architecture === 'all' ||
650
+ candidate.multiArch === 'foreign'
651
+ );
652
+ }
653
+
654
+ function relationCandidates(atom, dependent, installedByName, providers) {
655
+ const candidates = [];
656
+ for (const item of installedByName.get(atom.name) || []) {
657
+ if (
658
+ architectureMatches(item, atom.qualifier, dependent.architecture) &&
659
+ versionSatisfies(item.version, atom.relation, atom.version)
660
+ ) {
661
+ candidates.push(item);
662
+ }
663
+ }
664
+ for (const provider of providers.get(atom.name) || []) {
665
+ if (
666
+ architectureMatches(provider.item, atom.qualifier, dependent.architecture) &&
667
+ (!atom.relation ||
668
+ (provider.version && versionSatisfies(provider.version, atom.relation, atom.version)))
669
+ ) {
670
+ candidates.push(provider.item);
671
+ }
672
+ }
673
+ return [...new Map(candidates.map((item) => [item.identity, item])).values()].sort(
674
+ (left, right) => compareText(left.identity, right.identity)
675
+ );
676
+ }
677
+
678
+ function dependencyRequirements(installed, issues) {
679
+ const installedByName = new Map();
680
+ for (const item of installed) {
681
+ const values = installedByName.get(item.name) || [];
682
+ values.push(item);
683
+ installedByName.set(item.name, values);
684
+ }
685
+ const providers = parseProvides(installed, issues);
686
+ const requirements = new Map();
687
+
688
+ for (const item of installed) {
689
+ const resolved = [];
690
+ for (const [field, type] of [
691
+ ['preDepends', 'pre-depends'],
692
+ ['depends', 'runtime'],
693
+ ]) {
694
+ if (!item[field]) continue;
695
+ for (const [groupIndex, group] of item[field].replaceAll('\n', ' ').split(',').entries()) {
696
+ const alternatives = group.split('|').map(parseRelationAtom);
697
+ if (alternatives.some((atom) => atom == null)) {
698
+ issue(
699
+ issues,
700
+ 'dependency-relation-invalid',
701
+ `status.${item.identity}.${field}[${groupIndex}]`,
702
+ 'Dependency relation uses unsupported or malformed binary-package syntax.'
703
+ );
704
+ resolved.push({ id: missingComponentId(item.identity, group), type });
705
+ continue;
706
+ }
707
+ let selected = null;
708
+ for (const atom of alternatives) {
709
+ const candidates = relationCandidates(atom, item, installedByName, providers);
710
+ if (candidates.length > 1) {
711
+ issue(
712
+ issues,
713
+ 'dependency-provider-ambiguous',
714
+ `status.${item.identity}.${field}[${groupIndex}]`,
715
+ 'Dependency relation resolves to multiple installed providers.'
716
+ );
717
+ }
718
+ if (candidates.length > 0) {
719
+ selected = candidates[0];
720
+ break;
721
+ }
722
+ }
723
+ if (!selected) {
724
+ issue(
725
+ issues,
726
+ 'dependency-unsatisfied',
727
+ `status.${item.identity}.${field}[${groupIndex}]`,
728
+ 'No installed package satisfies this dependency group.'
729
+ );
730
+ resolved.push({ id: missingComponentId(item.identity, group), type });
731
+ } else {
732
+ resolved.push({ id: selected.id, type });
733
+ }
734
+ }
735
+ }
736
+ requirements.set(
737
+ item.identity,
738
+ [...new Map(resolved.map((entry) => [`${entry.id}\0${entry.type}`, entry])).values()].sort(
739
+ (left, right) => compareText(left.id, right.id) || compareText(left.type, right.type)
740
+ )
741
+ );
742
+ }
743
+ return requirements;
744
+ }
745
+
746
+ function normalizeRoot(root, snapshot, issues) {
747
+ const id = validId(root?.id) ? root.id : null;
748
+ const version = validDebianVersion(root?.version) ? root.version : null;
749
+ const custodyMode = root?.custody?.mode;
750
+ const custodyOwner = validId(root?.custody?.owner) ? root.custody.owner : null;
751
+ const trustDomain = validId(root?.custody?.trustDomain) ? root.custody.trustDomain : null;
752
+ const sourceUri = portableSource(root?.source?.uri) ? root.source.uri : null;
753
+ const sourceRevision = pinnedRevision(root?.source?.revision) ? root.source.revision : null;
754
+ if (!id) issue(issues, 'root-id-invalid', 'root.id', 'Root id must be a portable identifier.');
755
+ if (!version)
756
+ issue(issues, 'root-version-not-pinned', 'root.version', 'Root version must be exact.');
757
+ if (custodyMode !== 'owned' && custodyMode !== 'external') {
758
+ issue(issues, 'root-custody-invalid', 'root.custody.mode', 'Root custody must be explicit.');
759
+ }
760
+ if (!custodyOwner)
761
+ issue(issues, 'root-owner-invalid', 'root.custody.owner', 'Root custody owner is required.');
762
+ if (!trustDomain)
763
+ issue(
764
+ issues,
765
+ 'root-trust-domain-invalid',
766
+ 'root.custody.trustDomain',
767
+ 'Root trust domain is required.'
768
+ );
769
+ if (!sourceUri)
770
+ issue(
771
+ issues,
772
+ 'root-source-not-portable',
773
+ 'root.source.uri',
774
+ 'Root source must be a portable URI.'
775
+ );
776
+ if (!sourceRevision)
777
+ issue(
778
+ issues,
779
+ 'root-revision-not-pinned',
780
+ 'root.source.revision',
781
+ 'Root source revision must be pinned.'
782
+ );
783
+ return {
784
+ id,
785
+ kind: 'debian-system-root',
786
+ version,
787
+ custody: {
788
+ mode: custodyMode === 'owned' || custodyMode === 'external' ? custodyMode : null,
789
+ owner: custodyOwner,
790
+ trustDomain,
791
+ },
792
+ source: { uri: sourceUri, revision: sourceRevision },
793
+ artifact: { digest: hashJson(snapshot) },
794
+ execution: { installScripts: 'none' },
795
+ requires: [],
796
+ verification: { rebuilds: [] },
797
+ };
798
+ }
799
+
800
+ export function importDebianPackageSnapshot({
801
+ status,
802
+ sources = null,
803
+ packagesIndex,
804
+ maintainerScripts,
805
+ repository,
806
+ root,
807
+ verificationPolicy = null,
808
+ externalCustody = { owner: 'debian-archive', trustDomain: 'debian-archive' },
809
+ now = new Date(),
810
+ } = {}) {
811
+ const issues = [];
812
+ const sanitizedVerificationPolicy = sanitizeVerificationPolicy(verificationPolicy, issues);
813
+ const statusParagraphs = parseControl(status, 'status', issues);
814
+ const installed = normalizeInstalledPackages(statusParagraphs, issues);
815
+ const repositorySources = buildRepositorySources(
816
+ { sources, packagesIndex, repository, externalCustody },
817
+ installed,
818
+ issues,
819
+ now
820
+ );
821
+ const packageIndex = repositorySources.packageIndex;
822
+ const scripts = normalizeMaintainerScripts(maintainerScripts, installed, issues);
823
+ const requirements = dependencyRequirements(installed, issues);
824
+
825
+ const matchedPackages = [];
826
+ for (const item of installed) {
827
+ const key = `${item.identity}@${item.version}`;
828
+ const repositoryEntry = packageIndex.get(key) || null;
829
+ if (!repositoryEntry) {
830
+ issue(
831
+ issues,
832
+ 'repository-package-missing',
833
+ `packagesIndex.${item.identity}`,
834
+ 'Installed package version and architecture are absent from the Packages index.'
835
+ );
836
+ }
837
+ matchedPackages.push({
838
+ architecture: item.architecture,
839
+ digest: repositoryEntry?.digest || null,
840
+ filename: repositoryEntry?.filename || null,
841
+ identity: item.identity,
842
+ packagesIndexDigest: repositoryEntry?.packagesIndexDigest || null,
843
+ repositoryUri: repositoryEntry?.repositoryUri || null,
844
+ scripts: scripts[item.identity] || {},
845
+ version: item.version,
846
+ });
847
+ }
848
+ matchedPackages.sort((left, right) => compareText(left.identity, right.identity));
849
+
850
+ const snapshot = {
851
+ installed: installed.map((item) => ({
852
+ architecture: item.architecture,
853
+ depends: item.depends,
854
+ identity: item.identity,
855
+ multiArch: item.multiArch,
856
+ preDepends: item.preDepends,
857
+ provides: item.provides,
858
+ version: item.version,
859
+ })),
860
+ packages: matchedPackages,
861
+ repositories: repositorySources.evidence,
862
+ };
863
+ const normalizedRoot = normalizeRoot(root, snapshot, issues);
864
+ normalizedRoot.requires = installed
865
+ .filter((item) => item.id)
866
+ .map((item) => ({ id: item.id, type: 'operating-system' }))
867
+ .sort((left, right) => compareText(left.id, right.id));
868
+
869
+ const components = [normalizedRoot];
870
+ for (const item of installed) {
871
+ const repositoryEntry = packageIndex.get(`${item.identity}@${item.version}`) || null;
872
+ const packageScripts = scripts[item.identity] || {};
873
+ components.push({
874
+ id: item.id,
875
+ kind: 'debian-package',
876
+ version: item.version,
877
+ custody: repositoryEntry?.custody || {
878
+ mode: 'external',
879
+ owner: null,
880
+ trustDomain: null,
881
+ },
882
+ source: {
883
+ uri: repositoryEntry?.sourceUri || null,
884
+ revision: repositoryEntry?.digest || null,
885
+ },
886
+ artifact: { digest: repositoryEntry?.digest || null },
887
+ execution: {
888
+ installScripts: Object.keys(packageScripts).length > 0 ? 'present' : 'none',
889
+ },
890
+ requires: requirements.get(item.identity) || [],
891
+ verification: { rebuilds: [] },
892
+ });
893
+ }
894
+ components.sort((left, right) => compareText(left.id, right.id));
895
+
896
+ if (
897
+ normalizedRoot.id &&
898
+ components.some(
899
+ (component) => component !== normalizedRoot && component.id === normalizedRoot.id
900
+ )
901
+ ) {
902
+ issue(
903
+ issues,
904
+ 'component-id-collision',
905
+ 'root.id',
906
+ 'Root id collides with a generated package component id.'
907
+ );
908
+ }
909
+ issues.sort(
910
+ (left, right) => compareText(left.path, right.path) || compareText(left.code, right.code)
911
+ );
912
+
913
+ const dependencies = components.reduce(
914
+ (count, component) => count + component.requires.length,
915
+ 0
916
+ );
917
+ const maintainerScriptPackages = components.filter(
918
+ (component) =>
919
+ component.kind === 'debian-package' && component.execution.installScripts === 'present'
920
+ ).length;
921
+ const importable = issues.length === 0;
922
+ const input = {
923
+ root: normalizedRoot.id,
924
+ coverage: {
925
+ includedLayers: repositorySources.allAuthenticated
926
+ ? ['operating-system', 'repository-authentication']
927
+ : ['operating-system'],
928
+ missingLayers: repositorySources.allAuthenticated
929
+ ? ['native-build']
930
+ : ['native-build', 'repository-authentication'],
931
+ },
932
+ verificationPolicy: sanitizedVerificationPolicy,
933
+ components,
934
+ };
935
+ const receipt = {
936
+ schema: HOLOSYSTEM_SUBSTRATE_IMPORT_SCHEMA,
937
+ generatedAt: now.toISOString(),
938
+ status: importable
939
+ ? maintainerScriptPackages > 0
940
+ ? 'execution-policy-required'
941
+ : 'coverage-and-attestation-required'
942
+ : 'blocked',
943
+ importable,
944
+ source: {
945
+ format: 'debian-package-snapshot',
946
+ repositories: repositorySources.evidence,
947
+ statusHash: hashJson(snapshot.installed),
948
+ maintainerScriptManifestHash: hashJson(scripts),
949
+ },
950
+ summary: {
951
+ components: components.length,
952
+ dependencies,
953
+ installedPackages: installed.length,
954
+ matchedRepositoryPackages: matchedPackages.filter((item) => item.digest).length,
955
+ missingAttestations: components.length,
956
+ maintainerScriptPackages,
957
+ issues: issues.length,
958
+ },
959
+ evidence: { maintainerScripts: scripts },
960
+ input,
961
+ issues,
962
+ boundaries: {
963
+ packagesIndexDigestIsCallerAnchorNotReleaseSignatureProof:
964
+ !repositorySources.allAuthenticated,
965
+ releaseSignaturesAndIndexHashChainsVerified: repositorySources.allAuthenticated,
966
+ gpgvBinaryKeyringsAndFingerprintsRemainCallerTrustAnchors: repositorySources.allAuthenticated,
967
+ installedStatusIsNotRuntimeBehaviorProof: true,
968
+ generatedComponentsRequireSignedAttestations: true,
969
+ archiveCustodyRemainsExternal: true,
970
+ maintainerScriptsWereHashedNotExecuted: true,
971
+ maintainerScriptsBlockSubstrateClosure: true,
972
+ dependencyAlternativesResolveOnlyFromInstalledState: true,
973
+ circularDependsMayRequireExplicitBootstrapPolicy: true,
974
+ nativeBuildDependenciesAreNotDerived: true,
975
+ },
976
+ };
977
+ receipt.receiptHash = hashReceipt({ ...receipt, generatedAt: null });
978
+ return receipt;
979
+ }