MaCocoa 020

Capitolo 020 - Ricominciamo!

Ricomincio a lavorare con Cocoa riprendendo in mano le vecchie cose, operando qualche modifica, ed accorgendomi che ci sono alcune cose nuove.

Sorgenti: Cosmetica e rilettura documentazione sparsa.

Primo inserimento: 2 settembre 2002

Rileggendo i file

Per riprendere confidenza con la faccenda dopo tutto questo tempo, decido di rivedere passo passo tutto il codice, vedendo se c'è qualcosa da aggiustare o che non capisco.

Riprendo allora la classe LSFileInfo e vedo se ci sono alcune cose da mettere a punto.

Vado nella documentazione e trovo quanto segue sugli attributi di un file:

Value Type
NSFileSize (in bytes) NSNumber
NSFileModificationDate NSDate
NSFileOwnerAccountName NSString
NSFileGroupOwnerAccountName NSString
NSFileReferenceCount (number of hard links) NSNumber
NSFileIdentifier NSNumber
NSFileDeviceIdentifier NSNumber
NSFilePosixPermissions NSNumber
NSFileType NSString
NSFileExtensionHidden NSNumber containing a boolean
NSFileHFSCreatorCode NSNumber containing an unsigned long
NSFileHFSTypeCode NSNumber containing an unsigned long

I primi quattro attributi sono facili da capire: la dimensione del file stesso, quando è stato modificato l'ultima volta, gli attributi relativi ai diritti di proprietà dell'utente e del gruppo cui appartiene l'utente. Anche gli ultimi due attributi sono chiari: si tratta dei vecchi codici utilizzati in Mac OS 9 (e precedenti) per identificare il tipo ed il creatore del file. Queste informazioni erano utili al sistema operativo per mostrare la corretta icona nel Finder e per lanciare l'applicazione più appropriata per il trattamento del file. Queste stesse informazioni non sono più utili in Mac OS X in quanto sono ricavate per altra via (dall'estensione del file, per esempio). Quindi, il fatto che un file presenti questi due campi non vuoti è sintomo che deriva da una installazione precedente del sistema operativo (ma non necessariamente: le applicazioni carbonizzate mantengono tipo e creatore).

NSFileType e NSPosixPermission derivano dalla natura Unix di Mac OS X. In particolare NSFileType permette di discriminare la natura del file. Ora, come dovrebbe essere noto, nel mondo Unix tutto è un file: sotto la nozione di file entrano tranquillamente le directory (un file che colleziona altri file), i socket (una porta di comunicazione con il mondo esterno), i terminali video (ancora, una porta di comunicazione con il mondo), e sono tutte trattate logicamente allo stesso modo. Un file si apre o si crea, si scrive o si legge, si chiude quando si ha finito. Ecco le possibilità:

String Meaning
NSFileTypeUnknown Unknown file type
NSFileTypeCharacterSpecial Character special file
NSFileTypeDirectory Directory
NSFileTypeBlockSpecial Block special file
NSFileTypeRegular Regular file
NSFileTypeSymbolicLink Symbolic link
NSFileTypeSocket Socket

In realtà, non sono riuscito a trovare un utilizzo sensato di questo campo. In effetti, a meno che non si voglia catalogare il disco su cui si trova il sistema operativo, si troveranno solamente file normali e directory, in quanto tutti i file di sistema sono abilmente mascherati dal Finder, che non li visualizza.

(Vedi comunque qui per altre considerazioni in merito).

Mi rimangono tre campi. NSFileReferenceCount dice di quanti collegamenti quel file è riferimento. Si tratta di collegamenti 'hard' di stampo Unix, che sono simili, ma non la stessa cosa, degli alias cui si è abituati. Normalmente mi aspetto un valore di 1 (il file stesso), a mano che si tratti di file speciali di sistema.

(Sempre qui si discute dei link in Mac Os X).

Rimangono poi altri due campi, NSFileIdentifier e NSFileDeviceIdentifier, di cui nulla so (e che subito scopriremo non esistere...).

Imparato questo, mi limito ad aggiungere i campi che ancora non avevo considerato alla mia struttura di LSFileInfo, per renderla più completa.

figura 01

figura 01

Ovviamente, messo alla prova il codice con il debugger, vado incontro a delle sorprese. I due campi sopra sconosciuti sono veramente sconosciuti. Un semplice controllo col debugger mostra che il dizionario degli attributi restituito dall'istruzione

NSDictionary *fattrs = [manager fileAttributesAtPath: aFile traverseLink:NO];

riporta quanto segue (per i curiosi, è l'applicazione 'Calcolatrice'):

Printing description of fattrs:
{
    NSFileCreationDate = 2005-03-21 04:37:05 +0100;
    NSFileExtensionHidden = 1;
    NSFileGroupOwnerAccountID = 80;
    NSFileGroupOwnerAccountName = admin;
    NSFileModificationDate = 2005-03-21 04:37:05 +0100;
    NSFileOwnerAccountID = 0;
    NSFileOwnerAccountName = root;
    NSFilePosixPermissions = 509;
    NSFileReferenceCount = 3;
    NSFileSize = 102;
    NSFileSystemFileNumber = 14671;
    NSFileSystemNumber = 234881033;
    NSFileType = NSFileTypeDirectory;
}

Nessuna presenza dei due campi incriminati, sostituiti da due altrettanto sconosciuti NSFileSystemFileNumber e NSFileSystemNumber.

Riscrivo il mio codice per tenere conto di questi due campi, anche se al momento non so proprio che farci (ho il tragico sospetto che abbiano a che fare con il file system e la posizione del file all'interno del file system stesso: esaminando i numeri prodotti da altri file, NSFileSystemNumber non cambia, mentre NSFileSystemFileNumber varia).

(Cosa assolutamente non vera: su uno stesso volume ho trovato file con NSFileSystemNumber con valori differenti...).

Fatti e formati nuovi

Una cosa interessante è invece il fatto che sono comparsi (o non li avevo visti prima) dei metodi di comodo per leggere elementi dal dizionario che contiene gli attributi del file. Alcuni di questi metodi non sono nemmeno documentati, ma presenti nel file di interfaccia NSFileManager.h. Con tutto ciò, e tenendo conto di tutta una serie di interventi cosmetici, ho in pratica ristrutturato i file LSFileInfo.h e LSFileInfo.m, cambiando in particolare le variabili d'istanza e i relativi metodi accessor. Ricordo che il tipo OSType altri non è che un long.

Da ricordare (altrimenti si perdono i dati salvando ed aprendo i file) i due metodi di codifica/decodifica. Questi ultimi due metodi dicono anche una cosa interessante: i file salvati in precedenza non sono più compatibili con questa versione, in quanto adesso la struttura dati salvata è piuttosto diversa. Volendo fare i precisi, si poteva prevedere all'inizio del file un codice di riconoscimento della versione di file, oppure si poteva cambiare il suffisso del file. Data la pochezza dell'applicazione, non me ne cruccio, ma un programma commerciale che si rispetti dovrebbe prevedere la lettura di ogni versione precedente di file salvato (vi vengono in mente programmi che così non fanno? Ci sono cattivi programmatori...).

La presenza (o meglio, la rinnovata considerazione) del campo NSFilePosixPermissions mi ha indotto a costruire una classe Formatter per questa rappresentazione. Per capirci, quando fate ls -la sul terminale, i permessi di lettura/scrittura sono rappresentati dalla stringa di 'r','w' e 'x' all'inizio di ogni riga (in realtà, gli ultimi nove caratteri di una stringa di dieci: il primo carattere indica che si tratta di un file normale con '-' piuttosto che di una directory con 'd' o altro):

host191-123:~/Temporaneo djzero$ ls -l
total 8
drwxr-xr-x   10 djzero djzero 340 Aug  9 19:01 Tevac widget
-rw-r--r--    1 djzero djzero 1809 Aug 8 21:42 alienrss.xml

Qui si dice che il file 'Tevac widget' può essere letto ('r'), scritto ('w') ed eseguito ('x') dal possessore djzero (dando luogo ai primi tre caratteri 'rwx'), solo letto ed eseguito dal gruppo (e quindi 'r-x') e da tutti gli altri (chiudendo con 'r-x'). Queste stesse informazioni, in bella copia, si trovano nella finestra di Info che si può attivare dal Finder, nella parte relativa ai privilegi.

figura 02

figura 02

Detto questo, la classe formatter piglia il long che codifica questi privilegi e la trasforma proprio nella stringa di nove caratteri sopra considerata, tendo conto che ogni carattere deriva in pratica dal fatto che un bit sia ad uno piuttosto che a zero:

- (NSString *)
stringForObjectValue:    (id)anObject
{
    char        permstring[10] ;
    unsigned long     perm ;
    
    // controllo che l'oggetto sia un numero...
    if (![anObject isKindOfClass:[NSNumber class]]) {
        return nil;
    }
    // ricavo il long che mi da i permessi
    perm = [ anObject longValue ] ;
    // e poi e' tutto un sottile gioco di bit
    permstring[0] = ( perm & 0x0100 ) ? 'r' : '-' ;
    permstring[1] = ( perm & 0x0080 ) ? 'w' : '-' ;
    permstring[2] = ( perm & 0x0040 ) ? 'x' : '-' ;
    permstring[3] = ( perm & 0x0020 ) ? 'r' : '-' ;
    permstring[4] = ( perm & 0x0010 ) ? 'w' : '-' ;
    permstring[5] = ( perm & 0x0008 ) ? 'x' : '-' ;
    permstring[6] = ( perm & 0x0004 ) ? 'r' : '-' ;
    permstring[7] = ( perm & 0x0002 ) ? 'w' : '-' ;
    permstring[8] = ( perm & 0x0001 ) ? 'x' : '-' ;
    // chiudo la stringa C
    permstring[9] = 0 ;
    // converto la stringa C in NSString e la restituisco
    return ( [ NSString stringWithCString: permstring ] );
}

(Ci sono modifiche a quanto qui detto in questo capitolo).

Dimensioni strane

figura 03

figura 03

Passo adesso a gettare qualche luce (ma non risolvere) il problema delle dimensioni di un file. Tornando all'esempio precedente del player DVD, la dimensione riportata è di 208190. Niente di più sbagliato, almeno a sentire il Finder: costui, aprendo la finestra delle Info, dice che l'applicazione è di 480177 byte, più del doppio. Che fine hanno fatto i byte mancanti? Aprendo una finestra del Terminale, la dimensione riportata continua ad essere 200K (l'esempio è nel listato già visto sopra). Del resto, tornando al System 9, la dimensione definitiva dell'applicazione è di 480K. Il fatto è che il Terminale e il metodo Cocoa riportano la sola dimensione della data fork (come si può evincere usando una qualsiasi utility che riporti queste informazioni, un nome a caso, File Buddy). Per i file nativi Mac Os X, che non risentono della 'vecchia' ripartizione tra data fork e resource fork, le dimensioni sono sempre corrette e congruenti tra tutti im etodi di ispezione.

Questa storia era vera quando scrivevo la prima volta questa pagina. La cosa era dovuta al fatto che molte applicazioni erano ancora scritte utilizzando Carbon, e costruite per girare sotto Classic e Mac OS X; attualmente, con Tiger, le applicazioni sono scritte sostanzialmente in Cocoa o equivalente, e il problema dello spazio mancante, dovuto essenzialmente alla resource fork, è scomparso; tuttavia, lascio qui considerazioni e figure come utile discussione.

Per risolvere il problema della dimensione, temo non ci sarà altra possibilità che ricorrere a Carbon, ovvero alle funzioni che si interfacciano direttamente con il sistema operativo.... (ma per ora lascio perdere).

La cosa avverrà in capitolo successivo.

Questa investigazione mi ha fatto venire in mente una simpatica modifica che posso fare al programma, affinché la dimensioni di una directory non siano proprio quelle fisiche (che in effetti interessano a pochi), ma che riporti la dimensione occupata da tutti i file in essa compresi (e in generale in tutta l'alberatura che sta nella directory stessa). Questo giochetto, a prima vista piuttosto noioso, si risolve in realtà elegantemente grazie alla procedura ricorsiva utilizzata per aggiungere file all'alberatura.

Già che ci sono, introduco una modifica nel meccanismo di filtraggio. A suo tempo, avevo escluso dall'esplorazione le directory il cui nome terminava con .app, ovvero, evitavo di esplorare il contenuto delle applicazioni. Questo fatto però comporta che la dimensione dell'applicazione è quella del file directory, che in genere non è quello che ci interessa. Sarebbe meglio conoscere per intero lo spazio occupato dall'applicazione e dai suoi file accessori. Quindi, mi ritrovo a dover considerare tutti i file, ma di procedere alla visualizzazione di solamente una parte (quei file il cui nome non comincia per '.'), e dell'espansione di una parte (si espandono le directory ma non le applicazioni).

A questo punto, è bene parlare dei bundle: in Mac OS X il concetto di bundle è assimilabile a quello di directory specializzata a contenere codice e altre risorse specifiche per l'esecuzione di quel codice. Molte cose di utilizzo comune sono dei bundle: ad esempio una applicazione è un bundle; una libreria è un bundle; un plug-in (che so, di Photoshop) è un bundle. Un bundle accorpa in una unico posto tutte le risorse necessarie al funzionamento del codice contenuto: in questo modo si evita di disperdere file in giro per il disco (ad esempio, i file di help), e le applicazioni si copiano e si installano senza eccessivi problemi.

Il mio problema è come riconoscere un bundle. Infatti, vorrei poter visualizzare nell'elenco il nome del bundle, ed evitare di espanderlo (ché il contenuto di una applicazione, con risorse e quant'altro raramente mi interessa), pur continuando a darne la dimensione, in byte, corretta (derivante dalla somma di tutto ciò che vi è dentro). Ebbene, la documentazione (System Overview, lo avete da qualche parte all'interno della gerarchia /Developer) afferma che il Finder riconosce i bundle (e quindi mostra l'icona dell'applicazione piuttosto che l'icona di una cartella generica) da un bit nel suo database, o dall'estensione. Poiché a me il bit non lo fornisce nessuno (ed ignoro come si possa prelevare), tutto quello che posso fare è riconoscere l'estensione del nome del file: sono bundle tutte le cartelle il cui nome termina per .app, .framework, .bundle.

Nuovo inizializzatore

Mi accingo a riscrivere (parte) del metodo initTreeFromPath: della classe FileStruct per tenere conto delle dimensioni delle directory.

- (id) initTreeFromPath: (NSString*) fullPath
{
    // mi metto via un file manager
    NSFileManager     *fileManager = [NSFileManager defaultManager];
    BOOL        isAdir, fileOK ;
    int        i ;

    // per prima cosa, inizializzo super
    [ super initWithPath: fullPath ] ;
    // e adesso, se il file puntato e' una directory, espando
    fileOK = [ fileManager fileExistsAtPath: fullPath isDirectory: & isAdir ];
    // espando sempre, purche' il file esista e sia una directory
    if ( fileOK && isAdir)
    {
        // variabile per tenere traccia della dimensione parziale
        long    dirSize = 0;
        // vettore destinato a contenere i file
        NSMutableArray     *tmpfileList ;
        // l'elenco dei file
        NSArray *dirContent = [ fileManager directoryContentsAtPath: fullPath ];
        // conto quanti file ci sono all'interno
        int numFile = [ dirContent count ];
        // costruisco un vettore di dimensione adatta
        tmpfileList = [[ NSMutableArray alloc ] initWithCapacity: numFile ] ;
        // adesso, per ogni elemento, costruisco l'albero e poi lo aggiungo
        for ( i = 0; i < numFile; i++)
        {
            FileStruct    * tmpFile ;
            NSString     * myfile = [ dirContent objectAtIndex: i ] ;

            // costruisco l'albero
            tmpFile = [[FileStruct alloc] initTreeFromPath:
                [ fullPath stringByAppendingPathComponent: myfile]] ;
            // aggiungo la dimensione (cumulata) alla directory corrente
            dirSize += [ tmpFile fileSize ];
            // aggiungo il file alla lista
            [tmpfileList addObject: tmpFile ];
        }
        // assegna al file la nuova dimensione
        [ self setFileSize: dirSize ];
        // adesso copio il vettore con la giusta dimensione
        fileList = [[ NSMutableArray alloc ] initWithCapacity: [ tmpfileList count] ];
        [ fileList setArray: tmpfileList ];
    }
    else
    {
        // va beh, e' un file normale
        fileList = nil ;
    }
    return ( self );
}

Il codice mi pare sufficientemente commentato da non aver bisogno di ulteriore commento. Vogliate solo apprezzare l'eleganza della ricorsione, che calcola correttamente le dimensioni cumulate delle directory comunque annidate siano l'una dentro l'altra (gioie dell'esplorazione in profondità degli alberi).

Compilo il tutto, correggo i molti errori (che voi qui non vedete, visto che li ho già tutti corretti), ed eseguo. In effetti la cosa funziona.

Beh, non proprio. Quando procedo alla cancellazione di qualche file, la dimensione della directory che lo contiene non è aggiornata... Dovrei sottrarre alla dimensione della directory le dimensioni del file cancellato. In realtà non so se sia una buona cosa: sto tenendo un catalogo, e la dimensione della directory non è cambiata solo perché io ho cancellato dal catalogo un file... Decido per semplicità (o pigrizia) di lasciare le cose come stanno.

Con questi, ed altri ritocchi cosmetici, ho finito anche con FileStruct (che ne è uscita un po' più semplice).

Nuovi filtri

A questo punto, il grosso lavoro di filtro deve essere svolto dalla classe LSDataSource. Ripeto un'altra volta i due problemi:

1. evitare di visualizzare alcune categorie di file; li chiamo dotFiles per ricordare che sono in pratica i file il cui nome comincia con il carattere punto.

2. evitare di espandere i bundle; sono i file che hanno delle particolari estensioni.

Con questo in mente, i metodi della classe hanno subito una pesante revisione, che ha portato all'introduzione di quattro interessanti funzioni:

BOOL            skipDotFile ( NSString * fileName ) ;
BOOL            checkIfBundleDirectory( NSString * fileName );
short         countNormalFiles( FileStruct * directory );
FileStruct *    getNormalFile ( FileStruct * directory, short index ) ;

La prima funzione è il filtro sui dotFiles; in pratica, esamina il nome del file e decide se il file partecipa alla festa oppure è lasciato alla porta:

BOOL
skipDotFile ( NSString * fileName )
{
    const char    * fn ;        // nome del file in C
    // trasformo il nome del file in una stringa C
    fn = [ fileName cString] ;
    // perche' poi cosi' escludo quelli che cominciano con '.'
    if (*fn == '.' )
        return ( TRUE ) ;
    // escludo anche i file che si chiamano "Icon\r"
    // non chiedete il perche' delle '\r'
    if ( [ fileName isEqual: @"Icon\r"] )
        return ( TRUE ) ;
    // ... altre condizioni di esclusione
    // - - - - - - - - - - - - - - - -
    return ( FALSE );
}

Nel fare le prove, mi sono imbattuto nel problema dei file Icona. Come è noto, nel MacOs9 si possono attribuire icone fantasiose alle cartelle; queste icone sono mantenute in un file (nascosto) all'interno della cartella stessa. Per evitare di mostrare questi file (di scarso interesse), li considero dotFiles e li elimino dalla visualizzazione. La cosa buffa (che non capisco) è il nome del file, che io pensavo fosse "Icon", ed invece possiede un carattere di ritorno carrello alla fine del nome (misteri dei sistemi operativi).

La seconda funzione verifica se una directory è da considerare un bundle o meno. Molto semplicemente, si guarda il nome:

BOOL
checkIfBundleDirectory( NSString * fileName )
{
    // per ricavare l'estensione, c'e' un metodo apposito
    if ( [ fileName hasSuffix: @".app"] )
        return ( YES );
    if ( [ fileName hasSuffix: @".bundle"] )
        return ( YES );
    if ( [ fileName hasSuffix: @".framework"] )
        return ( YES );
    return ( NO );
}

Le altre due funzioni trattano il problema posto da NSOutlineView. Queste classi, per visualizzare al proprio interno la lista gerarchica di elementi, necessitano di sapere per ogni elemento quanti sottoelementi possiede (quindi, di ogni directory, quanti sono i file contenuti nella directory) e di poter accedere all'elemento i-esimo contenuto. La cosa è complicata dal fatto che nella conta sono considerati anche i dotFiles da non visualizzare. Per tanto, la terza funzione conta i file presenti in una directory saltando i dotFiles:

short
countNormalFiles( FileStruct * directory )
{
    short    i, count ;

    count = 0 ;    // all'inizio, nessuno
    for ( i = 0 ; i < [directory numOfFiles] ; i++ )
    {
        // se devo saltarlo, vado avanti senza incrementare il contatore
        if ( skipDotFile( [[directory getFileAtIndex:i] fileName ] ) )
            continue ;
        // se arrivo qui, e' un file normale, incremento il contatore
        count += 1 ;
    }
    return ( count );
}

La quarta funzione restituisce l'i-esimo file nella directory, continuando a saltare i dotFiles:

FileStruct *
getNormalFile ( FileStruct * directory, short index )
{
    short    i, count ;

    count = 0 ;    // contatore corrente
    for ( i = 0 ; i < [directory numOfFiles] ; i++ )
    {
        FileStruct * currFile ;
        // recupero il file
        currFile = [directory getFileAtIndex:i] ;
        // se devo saltarlo, passo avanti
        if ( skipDotFile( [ currFile fileName ] ) )
            continue ;
        // sono arrivato all'indice richiesto ?
        if ( count == index )
            // si, restituisco l'elemento corrente
            return ( currFile );
        // se arrivo qui, non ho ancora raggiunto l'elemento
        count += 1 ;
    }
    // ovviamente, qui non dovrei mai arrivare...
    return ( nil) ;
}

A questo punto, i due metodi necessari a NSOutlineview diventano

- (int)    
outlineView:            (NSOutlineView *)outlineView
    numberOfChildrenOfItem:    (id)item
{

    // se l'item e' nil, stiamo parlando della radice
    if (item == nil)
        // dico quindi che ci sono tanti elementi quanti presenti nel vettore
        return( [ [self startPoint] count ]);
    // se arrivo qui, mi si chiede quali figli ha un file
    // tratto subito i file normali, che non hanno figli
    if ( [item numOfFiles] == 0 )
        return ( 0 ) ;
    // se arrivo qui, ho una directory
    // devo eliminare dal computo i dotFiles
    // ed alora, mi tocca esaminare tutti i file e contare quelli
    // che mi interessano
    return ( countNormalFiles( item) ) ;
}

- (id)
outlineView:        (NSOutlineView *) outlineView
    child:        (int) index
    ofItem:        (id) item
{
    if (item == nil)
        return( [ [self startPoint] objectAtIndex: index ]);
    // devo saltare i dotfiles
    return ( getNormalFile ( item, index ) );
}

Infine, per vedere se un elemento di NSOutlineview è espandibile o meno, si utilizza:

- (BOOL)
outlineView:            (NSOutlineView *)outlineView
    isItemExpandable:    (id)item
{    
    if (item == nil)
        return ( YES ) ;
    // se non ha figli, e' un file, non si espande
    if ( [item numOfFiles] == 0 )
        return ( NO ) ;
    // se arrivo qui, l'item ha figli
    // espando allora i non bundle
    return (! checkIfBundleDirectory([ item fileName ]) ) ;
}

figura 04

figura 04

figura 05

figura 05

Nelle due immagini, l'aspetto prima e dopo la cura.

Non sono ancora soddisfatto. Non mi piace inserire direttamente nel codice una serie di scelte che in realtà sono proprie dell'utente, ovvero, se visualizzare o meno i bundle e se visualizzare o meno i dotFiles. Per questo motivo, voglio realizzare una finestra con cui l'utente possa assegnare delle preferenze di visualizzazione.

La politica di funzionamento sarà quindi che in sede di lettura dati (cioè, quando si aggiungono file al catalogo) sono comunque conteggiati tutti i file; in sede di visualizzazione, ci saranno dei filtri per visualizzare o meno determinati file o per espandere o meno determinate directory. Ma di questo ne parlo il prossimo capitolo, cui vi rimando anche per tutti i file ed il progetto.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).