@@ -30,7 +30,10 @@ export function isCustomFolderIconName(name: string): boolean {
3030const ICON_CACHE_DB = 'classicstack-icon-cache' ;
3131/** Bump when decoded-icon preference or extract rules change so stale BW PNGs are dropped. */
3232const ICON_CACHE_DB_VERSION = 3 ;
33- const HAS_CUSTOM_ICON = 0x0400 ;
33+ /** Finder FileInfo/DInfo: custom icon (Icon\\r or id -16455). */
34+ export const HAS_CUSTOM_ICON = 0x0400 ;
35+ /** Finder FileInfo: BNDL resources in the file's resource fork. */
36+ export const HAS_BUNDLE = 0x2000 ;
3437/** Finder FileInfo/DInfo flag: item is invisible (AppleDouble FinderInfo). */
3538export const FINDER_IS_INVISIBLE = 0x4000 ;
3639
@@ -84,6 +87,34 @@ export function isCdevStyleType(type: string): boolean {
8487 return CDEV_STYLE_TYPES . has ( padOsType ( type ) ) ;
8588}
8689
90+ /** True when this file's own resource fork (not a type/creator cache) may hold a Finder icon. */
91+ export function shouldReadIconFork (
92+ finderInfo : Uint8Array ,
93+ type : string ,
94+ cached ?: IconUrls | null ,
95+ ) : boolean {
96+ const flags = finderFlags ( finderInfo ) ;
97+ if ( ( flags & HAS_CUSTOM_ICON ) !== 0 ) return true ;
98+ if ( cached ) return false ;
99+ const t = padOsType ( type ) ;
100+ return ( flags & HAS_BUNDLE ) !== 0 || t === 'APPL' || isCdevStyleType ( t ) ;
101+ }
102+
103+ /** Ids / extract rules for a ranged AFP resource-fork read. */
104+ export function iconForkLoadOptions ( node : VNode ) : FinderIconForkOpts {
105+ const { type } = readTypeCreator ( node . finderInfo ) ;
106+ const flags = finderFlags ( node . finderInfo ) ;
107+ const extraIds : number [ ] = [ ] ;
108+ if ( isCdevStyleType ( type ) ) extraIds . push ( CDEV_ICON_ID ) ;
109+ if ( ( flags & HAS_CUSTOM_ICON ) !== 0 || isCustomFolderIconName ( node . name ) ) extraIds . push ( CUSTOM_ICON_ID ) ;
110+ const fid = finderIconId ( node . finderInfo ) ;
111+ if ( fid ) extraIds . push ( fid ) ;
112+ return {
113+ extraIds,
114+ includeAllIcons : isCustomFolderIconName ( node . name ) ,
115+ } ;
116+ }
117+
87118function isSystemIconUrls ( urls : IconUrls ) : boolean {
88119 return urls . small . startsWith ( '/icons/' ) || urls . large . startsWith ( '/icons/' ) ;
89120}
@@ -204,6 +235,8 @@ export class IconCache {
204235 private memory = new Map < string , IconUrls > ( ) ;
205236 private bundleCache = new Map < string , BundleCacheEntry > ( ) ;
206237 private dirMemory = new Map < string , IconUrls > ( ) ;
238+ private typeInflight = new Map < string , Promise < IconUrls > > ( ) ;
239+ private dirInflight = new Map < string , Promise < IconUrls > > ( ) ;
207240 private db : IDBPDatabase | null = null ;
208241 private initPromise : Promise < void > | null = null ;
209242 private systemReady : Promise < void > | null = null ;
@@ -241,6 +274,8 @@ export class IconCache {
241274 this . memory . clear ( ) ;
242275 this . bundleCache . clear ( ) ;
243276 this . dirMemory . clear ( ) ;
277+ this . typeInflight . clear ( ) ;
278+ this . dirInflight . clear ( ) ;
244279 await this . init ( ) ;
245280 if ( ! this . db ) return ;
246281 await this . clearStore ( 'typeIcons' ) ;
@@ -249,6 +284,7 @@ export class IconCache {
249284 /** Drop cached folder icons (e.g. after VirtualFS mutations / Icon\\r changes). */
250285 clearDirectoryCache ( ) : void {
251286 this . dirMemory . clear ( ) ;
287+ this . dirInflight . clear ( ) ;
252288 }
253289
254290 private async clearStore ( store : string ) : Promise < void > {
@@ -293,8 +329,36 @@ export class IconCache {
293329 return this . getForDirectory ( String ( node . id ) , node , findChild , loadIconFork ) ;
294330 }
295331 const { type, creator } = readTypeCreator ( node . finderInfo ) ;
332+ const key = cacheKey ( creator , type ) ;
333+ const cached = this . memory . get ( key ) ?? ( await this . loadPersisted ( key ) ) ;
334+ if ( cached ) this . memory . set ( key , cached ) ;
335+ const custom = ( finderFlags ( node . finderInfo ) & HAS_CUSTOM_ICON ) !== 0 ;
336+ if ( cached && ! custom ) return cached ;
337+
338+ if ( ! custom ) {
339+ const pending = this . typeInflight . get ( key ) ;
340+ if ( pending ) return pending ;
341+ }
342+
343+ const work = this . resolveFileNode ( node , type , creator , cached , loadIconFork ) ;
344+ if ( ! custom ) {
345+ this . typeInflight . set ( key , work ) ;
346+ void work . finally ( ( ) => {
347+ if ( this . typeInflight . get ( key ) === work ) this . typeInflight . delete ( key ) ;
348+ } ) ;
349+ }
350+ return work ;
351+ }
352+
353+ private async resolveFileNode (
354+ node : VNode ,
355+ type : string ,
356+ creator : string ,
357+ cached : IconUrls | null ,
358+ loadIconFork ?: ( node : VNode ) => Promise < ResourceFork | null > ,
359+ ) : Promise < IconUrls > {
296360 let fork : ResourceFork | null = null ;
297- if ( node . resource . length < 16 && loadIconFork ) {
361+ if ( node . resource . length < 16 && loadIconFork && shouldReadIconFork ( node . finderInfo , type , cached ) ) {
298362 try {
299363 fork = await loadIconFork ( node ) ;
300364 } catch {
@@ -315,6 +379,8 @@ export class IconCache {
315379 async getForTypeCreator ( type : string , creator : string ) : Promise < IconUrls > {
316380 await this . ensureDefaults ( ) ;
317381 const key = cacheKey ( creator , type ) ;
382+ const pending = this . typeInflight . get ( key ) ;
383+ if ( pending ) return pending ;
318384 const cached = this . memory . get ( key ) ?? ( await this . loadPersisted ( key ) ) ;
319385 if ( cached ) {
320386 this . memory . set ( key , cached ) ;
@@ -337,6 +403,36 @@ export class IconCache {
337403 const hit = this . dirMemory . get ( pathKey ) ;
338404 if ( hit ) return hit ;
339405
406+ // Only probe Icon\\r / the directory fork when the caller listed this
407+ // folder (findChild). Closed folders keep the default glyph until opened.
408+ if ( ! findChild ) {
409+ return (
410+ this . defaultFolder ?? {
411+ small : systemIconUrl ( 'DIR16.png' ) ,
412+ large : systemIconUrl ( 'DIR32.png' ) ,
413+ }
414+ ) ;
415+ }
416+
417+ const pending = this . dirInflight . get ( pathKey ) ;
418+ if ( pending ) return pending ;
419+ const work = this . probeDirectory ( pathKey , node , findChild , loadIconFork ) ;
420+ this . dirInflight . set ( pathKey , work ) ;
421+ void work . finally ( ( ) => {
422+ if ( this . dirInflight . get ( pathKey ) === work ) this . dirInflight . delete ( pathKey ) ;
423+ } ) ;
424+ return work ;
425+ }
426+
427+ private async probeDirectory (
428+ pathKey : string ,
429+ node : VNode ,
430+ findChild : ( parentId : number , name : string ) => Promise < VNode | undefined > ,
431+ loadIconFork ?: ( node : VNode ) => Promise < ResourceFork | null > ,
432+ ) : Promise < IconUrls > {
433+ const hit = this . dirMemory . get ( pathKey ) ;
434+ if ( hit ) return hit ;
435+
340436 // Classic Finder custom folder icon lives in a root file named "Icon\r"
341437 // (Icon + CR / 0x0D), resource id -16455.
342438 const fromIconFile = await this . tryCustomFolderIconFile ( node , findChild , loadIconFork ) ;
@@ -374,7 +470,7 @@ export class IconCache {
374470 small : systemIconUrl ( 'DIR16.png' ) ,
375471 large : systemIconUrl ( 'DIR32.png' ) ,
376472 } ;
377- // Do not cache defaults — allows Icon\r added later to be picked up.
473+ this . dirMemory . set ( pathKey , urls ) ;
378474 return urls ;
379475 }
380476
@@ -419,13 +515,18 @@ export class IconCache {
419515 } ) : Promise < IconUrls > {
420516 const key = cacheKey ( args . creator , args . type ) ;
421517 const rf = forkFromNode ( args . resource , args . data , args . fork ?? null ) ;
518+ const custom = ( finderFlags ( args . finderInfo ) & HAS_CUSTOM_ICON ) !== 0 ;
422519 const cached = this . memory . get ( key ) ?? ( await this . loadPersisted ( key ) ) ;
423- if ( cached && ! ( rf && isSystemIconUrls ( cached ) ) ) {
520+ if ( cached && ! custom ) {
424521 this . memory . set ( key , cached ) ;
425522 return cached ;
426523 }
427524
428525 if ( ! rf ) {
526+ if ( cached ) {
527+ this . memory . set ( key , cached ) ;
528+ return cached ;
529+ }
429530 const urls : IconUrls = {
430531 small : await resolveSystemIcon ( args . type , 16 ) ,
431532 large : await resolveSystemIcon ( args . type , 32 ) ,
@@ -443,8 +544,10 @@ export class IconCache {
443544 if ( set ) {
444545 const urls = await iconSetToUrls ( set ) ;
445546 if ( urls ) {
446- this . memory . set ( key , urls ) ;
447- await this . persist ( key , urls ) ;
547+ if ( ! custom ) {
548+ this . memory . set ( key , urls ) ;
549+ await this . persist ( key , urls ) ;
550+ }
448551 return urls ;
449552 }
450553 }
0 commit comments