@@ -113,6 +113,162 @@ export function extractMetadataTargets(metadata) {
113113 return Array . from ( targets . values ( ) ) ;
114114}
115115
116+ async function resolveLocalAsset ( metadataDir , key , digestCache ) {
117+ if ( ! key || typeof key !== 'string' ) {
118+ return null ;
119+ }
120+
121+ const candidates = [ ] ;
122+ const seen = new Set ( ) ;
123+
124+ const addCandidate = ( value ) => {
125+ if ( ! value || typeof value !== 'string' ) {
126+ return ;
127+ }
128+ const trimmed = value . trim ( ) ;
129+ if ( ! trimmed || seen . has ( trimmed ) ) {
130+ return ;
131+ }
132+ seen . add ( trimmed ) ;
133+ candidates . push ( trimmed ) ;
134+ } ;
135+
136+ addCandidate ( key ) ;
137+ const normalised = normaliseInstallerFileName ( key ) ;
138+ if ( normalised && normalised !== key ) {
139+ addCandidate ( normalised ) ;
140+ }
141+
142+ for ( const candidate of candidates ) {
143+ const candidatePath = path . resolve ( metadataDir , candidate ) ;
144+ const relative = path . relative ( metadataDir , candidatePath ) ;
145+ if ( relative . startsWith ( '..' ) || path . isAbsolute ( relative ) ) {
146+ continue ;
147+ }
148+
149+ try {
150+ const stats = await fs . stat ( candidatePath ) ;
151+ if ( ! stats . isFile ( ) ) {
152+ continue ;
153+ }
154+
155+ let digest = digestCache . get ( candidatePath ) ;
156+ if ( ! digest ) {
157+ digest = await computeLocalDigest ( candidatePath ) ;
158+ digestCache . set ( candidatePath , digest ) ;
159+ }
160+
161+ return {
162+ name : candidate ,
163+ path : candidatePath ,
164+ sha512 : digest . sha512 ,
165+ size : digest . size ,
166+ } ;
167+ } catch {
168+ // Ignore missing assets at this stage; verification will surface the issue.
169+ }
170+ }
171+
172+ return null ;
173+ }
174+
175+ async function repairLocalMetadataFile ( metadataPath , metadataDocument ) {
176+ if ( ! metadataDocument || typeof metadataDocument !== 'object' ) {
177+ return false ;
178+ }
179+
180+ const metadataDir = path . dirname ( metadataPath ) ;
181+ const digestCache = new Map ( ) ;
182+ let changed = false ;
183+
184+ const ensureEntryMatchesAsset = async ( entry ) => {
185+ if ( ! entry || typeof entry !== 'object' ) {
186+ return false ;
187+ }
188+
189+ const key = entry . url || entry . path ;
190+ const asset = await resolveLocalAsset ( metadataDir , key , digestCache ) ;
191+ if ( ! asset ) {
192+ return false ;
193+ }
194+
195+ let localChange = false ;
196+ if ( entry . url && entry . url !== asset . name ) {
197+ entry . url = asset . name ;
198+ localChange = true ;
199+ }
200+ if ( entry . path && entry . path !== asset . name ) {
201+ entry . path = asset . name ;
202+ localChange = true ;
203+ }
204+ if ( asset . sha512 && entry . sha512 !== asset . sha512 ) {
205+ entry . sha512 = asset . sha512 ;
206+ localChange = true ;
207+ }
208+ if ( typeof asset . size === 'number' && entry . size !== asset . size ) {
209+ entry . size = asset . size ;
210+ localChange = true ;
211+ }
212+ return localChange ;
213+ } ;
214+
215+ if ( Array . isArray ( metadataDocument . files ) ) {
216+ for ( const entry of metadataDocument . files ) {
217+ if ( await ensureEntryMatchesAsset ( entry ) ) {
218+ changed = true ;
219+ }
220+ }
221+ }
222+
223+ const primarySource =
224+ ( typeof metadataDocument . path === 'string' && metadataDocument . path . trim ( ) ) ||
225+ ( Array . isArray ( metadataDocument . files ) &&
226+ metadataDocument . files [ 0 ] &&
227+ ( metadataDocument . files [ 0 ] . path || metadataDocument . files [ 0 ] . url ) ) ;
228+
229+ const primaryAsset = await resolveLocalAsset ( metadataDir , primarySource , digestCache ) ;
230+ if ( primaryAsset ) {
231+ if ( metadataDocument . path !== primaryAsset . name ) {
232+ metadataDocument . path = primaryAsset . name ;
233+ changed = true ;
234+ }
235+ if ( metadataDocument . sha512 !== primaryAsset . sha512 ) {
236+ metadataDocument . sha512 = primaryAsset . sha512 ;
237+ changed = true ;
238+ }
239+ if ( typeof primaryAsset . size === 'number' && metadataDocument . size !== primaryAsset . size ) {
240+ metadataDocument . size = primaryAsset . size ;
241+ changed = true ;
242+ }
243+
244+ if ( ! Array . isArray ( metadataDocument . files ) || metadataDocument . files . length === 0 ) {
245+ metadataDocument . files = [
246+ {
247+ url : primaryAsset . name ,
248+ sha512 : primaryAsset . sha512 ,
249+ size : primaryAsset . size ,
250+ } ,
251+ ] ;
252+ changed = true ;
253+ }
254+ }
255+
256+ if ( Array . isArray ( metadataDocument . files ) ) {
257+ for ( const entry of metadataDocument . files ) {
258+ if ( await ensureEntryMatchesAsset ( entry ) ) {
259+ changed = true ;
260+ }
261+ }
262+ }
263+
264+ if ( changed ) {
265+ const serialised = YAML . stringify ( metadataDocument , { lineWidth : 0 } ) . trimEnd ( ) ;
266+ await fs . writeFile ( metadataPath , `${ serialised } \n` , 'utf8' ) ;
267+ }
268+
269+ return changed ;
270+ }
271+
116272const execFileAsync = promisify ( execFile ) ;
117273
118274export async function curlGet ( url , headers ) {
@@ -484,7 +640,7 @@ export async function runRemoteCheck({ owner, repo, tag, skipHttp, skipDownload,
484640 console . log ( '\nAll metadata files reference available, reachable assets with matching hashes.' ) ;
485641}
486642
487- async function runLocalCheck ( { directory, skipDownload } ) {
643+ async function runLocalCheck ( { directory, skipDownload, fixMetadata = false } ) {
488644 const resolvedDirectory = path . resolve ( directory ) ;
489645 const entries = await fs . readdir ( resolvedDirectory ) ;
490646 const metadataFiles = entries . filter (
@@ -500,12 +656,7 @@ async function runLocalCheck({ directory, skipDownload }) {
500656 let failures = false ;
501657 for ( const metadataFile of metadataFiles ) {
502658 const metadataPath = path . join ( resolvedDirectory , metadataFile ) ;
503- const source = await fs . readFile ( metadataPath , 'utf8' ) ;
504- const references = extractMetadataReferences ( source ) ;
505- const missing = [ ] ;
506- const mismatchedHashes = [ ] ;
507- const missingHashes = [ ] ;
508- const mismatchedSizes = [ ] ;
659+ let source = await fs . readFile ( metadataPath , 'utf8' ) ;
509660 let metadataDocument = null ;
510661 let parseError = null ;
511662
@@ -515,8 +666,26 @@ async function runLocalCheck({ directory, skipDownload }) {
515666 parseError = error instanceof Error ? error : new Error ( String ( error ) ) ;
516667 }
517668
669+ let repaired = false ;
670+ if ( fixMetadata && ! parseError && metadataDocument && typeof metadataDocument === 'object' ) {
671+ repaired = await repairLocalMetadataFile ( metadataPath , metadataDocument ) ;
672+ if ( repaired ) {
673+ source = await fs . readFile ( metadataPath , 'utf8' ) ;
674+ metadataDocument = YAML . parse ( source ) ;
675+ }
676+ }
677+
678+ const references = extractMetadataReferences ( source ) ;
679+ const missing = [ ] ;
680+ const mismatchedHashes = [ ] ;
681+ const missingHashes = [ ] ;
682+ const mismatchedSizes = [ ] ;
683+
518684 console . log ( `\nMetadata: ${ metadataFile } ` ) ;
519685 console . log ( ` Referenced files: ${ references . length } ` ) ;
686+ if ( repaired ) {
687+ console . log ( ' • Updated metadata checksums to match local assets.' ) ;
688+ }
520689
521690 for ( const reference of references ) {
522691 const targetPath = path . join ( resolvedDirectory , reference ) ;
@@ -616,9 +785,10 @@ async function main() {
616785 const localDir = args . local ?? null ;
617786 const skipHttp = Boolean ( args [ 'skip-http' ] ) ;
618787 const skipDownload = Boolean ( args [ 'skip-download' ] ) ;
788+ const fixMetadata = Boolean ( args [ 'fix-metadata' ] ) ;
619789
620790 if ( localDir ) {
621- await runLocalCheck ( { directory : localDir , skipDownload } ) ;
791+ await runLocalCheck ( { directory : localDir , skipDownload, fixMetadata } ) ;
622792 return ;
623793 }
624794
0 commit comments