|
| 1 | +import * as fs from 'fs/promises' |
| 2 | +import type { PatchManifest, PatchRecord } from '../schema/manifest-schema.js' |
| 3 | +import { PatchManifestSchema } from '../schema/manifest-schema.js' |
| 4 | + |
| 5 | +/** |
| 6 | + * Get all blob hashes referenced by a manifest (both beforeHash and afterHash) |
| 7 | + * Used for garbage collection and validation |
| 8 | + */ |
| 9 | +export function getReferencedBlobs(manifest: PatchManifest): Set<string> { |
| 10 | + const blobs = new Set<string>() |
| 11 | + |
| 12 | + for (const patchRecord of Object.values(manifest.patches)) { |
| 13 | + const record = patchRecord as PatchRecord |
| 14 | + for (const fileInfo of Object.values(record.files)) { |
| 15 | + blobs.add(fileInfo.beforeHash) |
| 16 | + blobs.add(fileInfo.afterHash) |
| 17 | + } |
| 18 | + } |
| 19 | + |
| 20 | + return blobs |
| 21 | +} |
| 22 | + |
| 23 | +/** |
| 24 | + * Get only afterHash blobs referenced by a manifest |
| 25 | + * Used for apply operations - we only need the patched file content, not the original |
| 26 | + * This saves disk space since beforeHash blobs are not needed for applying patches |
| 27 | + */ |
| 28 | +export function getAfterHashBlobs(manifest: PatchManifest): Set<string> { |
| 29 | + const blobs = new Set<string>() |
| 30 | + |
| 31 | + for (const patchRecord of Object.values(manifest.patches)) { |
| 32 | + const record = patchRecord as PatchRecord |
| 33 | + for (const fileInfo of Object.values(record.files)) { |
| 34 | + blobs.add(fileInfo.afterHash) |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + return blobs |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * Get only beforeHash blobs referenced by a manifest |
| 43 | + * Used for rollback operations - we need the original file content to restore |
| 44 | + */ |
| 45 | +export function getBeforeHashBlobs(manifest: PatchManifest): Set<string> { |
| 46 | + const blobs = new Set<string>() |
| 47 | + |
| 48 | + for (const patchRecord of Object.values(manifest.patches)) { |
| 49 | + const record = patchRecord as PatchRecord |
| 50 | + for (const fileInfo of Object.values(record.files)) { |
| 51 | + blobs.add(fileInfo.beforeHash) |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + return blobs |
| 56 | +} |
| 57 | + |
| 58 | +/** |
| 59 | + * Calculate differences between two manifests |
| 60 | + */ |
| 61 | +export interface ManifestDiff { |
| 62 | + added: Set<string> // PURLs |
| 63 | + removed: Set<string> |
| 64 | + modified: Set<string> |
| 65 | +} |
| 66 | + |
| 67 | +export function diffManifests( |
| 68 | + oldManifest: PatchManifest, |
| 69 | + newManifest: PatchManifest, |
| 70 | +): ManifestDiff { |
| 71 | + const oldPurls = new Set(Object.keys(oldManifest.patches)) |
| 72 | + const newPurls = new Set(Object.keys(newManifest.patches)) |
| 73 | + |
| 74 | + const added = new Set<string>() |
| 75 | + const removed = new Set<string>() |
| 76 | + const modified = new Set<string>() |
| 77 | + |
| 78 | + // Find added and modified |
| 79 | + for (const purl of newPurls) { |
| 80 | + if (!oldPurls.has(purl)) { |
| 81 | + added.add(purl) |
| 82 | + } else { |
| 83 | + const oldPatch = oldManifest.patches[purl] as PatchRecord |
| 84 | + const newPatch = newManifest.patches[purl] as PatchRecord |
| 85 | + if (oldPatch.uuid !== newPatch.uuid) { |
| 86 | + modified.add(purl) |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + // Find removed |
| 92 | + for (const purl of oldPurls) { |
| 93 | + if (!newPurls.has(purl)) { |
| 94 | + removed.add(purl) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + return { added, removed, modified } |
| 99 | +} |
| 100 | + |
| 101 | +/** |
| 102 | + * Validate a parsed manifest object |
| 103 | + */ |
| 104 | +export function validateManifest(parsed: unknown): { |
| 105 | + success: boolean |
| 106 | + manifest?: PatchManifest |
| 107 | + error?: string |
| 108 | +} { |
| 109 | + const result = PatchManifestSchema.safeParse(parsed) |
| 110 | + if (result.success) { |
| 111 | + return { success: true, manifest: result.data } |
| 112 | + } |
| 113 | + return { |
| 114 | + success: false, |
| 115 | + error: result.error.message, |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +/** |
| 120 | + * Read and parse a manifest from the filesystem |
| 121 | + */ |
| 122 | +export async function readManifest(path: string): Promise<PatchManifest | null> { |
| 123 | + try { |
| 124 | + const content = await fs.readFile(path, 'utf-8') |
| 125 | + const parsed = JSON.parse(content) |
| 126 | + const result = validateManifest(parsed) |
| 127 | + return result.success ? result.manifest! : null |
| 128 | + } catch { |
| 129 | + return null |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +/** |
| 134 | + * Write a manifest to the filesystem |
| 135 | + */ |
| 136 | +export async function writeManifest( |
| 137 | + path: string, |
| 138 | + manifest: PatchManifest, |
| 139 | +): Promise<void> { |
| 140 | + const content = JSON.stringify(manifest, null, 2) |
| 141 | + await fs.writeFile(path, content, 'utf-8') |
| 142 | +} |
0 commit comments