B8. Input/Output su file
Le classi base per l'I/O su file sono derivate dalla classe astratta Stream, facente parte del namespace System.IO. In questo capitolo
verranno analizzate StreamWriter e StreamReader per la modifica di file testuali, e BinaryWriter e BinaryReader per la modifica di file binari.
Come si è visto, le classi analizzate (e quelle che andremo a vedere tra breve) hanno metodi molti simili a quelli di Console: questo perchè anche la console è uno stream, capace di input e output allo stesso tempo. Per coloro che provengono dal C non sarà difficile richiamare questo concetto, ma per i neofiti della programmazione, fornisco una breve definizione (presa da Wikipedia), più generale, di stream.
Con l'acronimo API ci si riferisce all'Application Programming Interface di un linguaggio di programmazione, ossia l'insieme di tutti i metodi resi disponibili al programmatore. Globalmente parlando, quindi, si può associare uno stream al flusso di dati proveniente da un qualsiasi dispositivo virtuale o fisico o da qualunque entità astratta all'interno della macchina: ad esempio è possibile avere uno stream associato a una stampante, a uno scanner, allo schermo, ad un file, alla memoria temporanea, a qualsiasi altra cosa. Questo concetto verrà espresso meglio nel capitolo 84, parlando della comunicazione di due computer in rete, gestita attraverso uno stream condiviso. Tuttavia, il .Net Framework non mette a disposizione tutta questa potenza alla classe Stream, ma piuttosto la suddivide in più classi differenti, così abbiamo FileStream, Console, NetworkStream, MemoryStream e via dicendo.
Per prima cosa, si costruisca l'interfaccia grafica, con questi controlli:

Ed ecco il codice ampiamente commentato:
Lettura/scrittura di file testuali
I file testuali sono così denominati perchè contengono solo testo, ossia bytes codifcabili in una delle codifiche standard dei caratteri
(ASCII o UTF-8). Alcuni particolari bytes vengono intepretati in modi diversi, come ad esempio la tabulazione, che viene rappresentata con
uno spazio più lungo; altri vengono tralasciati nella visualizzazione e sembrano non esistere, ad esempio il NULL terminator, che rappresenta
la fine di una stringa, oppure l'EOF (End Of File); altri ancora vengono presi a gruppi, come il carattere a capo, che in realtà è formato
da una sequenza di due bytes (Carriage Return e Line Feed, rispettivamente 13 e 10). La differenza insita in questi tipi di file rispetto a
quelli binari è il fatto di non poter leggere i singoli bytes perchè non ce n'è necessità: quello che importa è l'informazione che il
testo porta al suo interno. La classe usata per la lettura è StreamReader, mentre quella per la scrittura StreamWriter: il costruttore di
entrambi accetta un unico parametro, ossia il percorso del file in questione; esistono anche altri overloads dei costruttori, ma il più
usato e quindi il più importante di tutti è quello appena citato. Ecco un piccolo esempio di come utilizzare tali classi in una semplice
applicazione console:
Ovviamente esistono anche i metodi Read e Write, che scrivono del testo senza mandare a capo: inoltre, Write e WriteLine hanno degli overloads che accettano anche stringhe di formato come quelle viste nel capitolo 31.Module Module1Sub Main()Dim FileAs String Dim ModeAs Char Console.WriteLine("Premere R per leggere un file, W per scriverne uno.") 'Console.ReadKey restituisce un oggetto ConsoleKeyInfo, al cui 'interno ci sono tre proprietà: Key, enumeratore che definisce 'il codice del pulsante premuto; KeyChar, il carattere 'corrispondente a quel pulsante; Modifier, enumeratore che 'definisce i modificatori attivi, ossia Ctrl, Shift e Alt. 'Quello che serve ora è solo KeyChar Mode = Console.ReadKey.KeyChar 'Dato che potrebbe essere attivo il Bloc Num, ci si assicura che 'Mode contenga un carattere maiuscolo con la funzione statica ToUpper 'del tipo base Char Mode =Char .ToUpper(Mode) 'Pulisce lo schermo Console.Clear()Select Case ModeCase "R" Console.WriteLine("Inserire il percorso del file da leggere:") File = Console.ReadLine 'Cosntrolla che il file esista If Not IO.File.Exists(File)Then 'Se non esiste, visualizza un messggio ed esce Console.WriteLine("Il file specificato non esiste!") Console.ReadKey()Exit Sub End If Dim ReaderAs New IO.StreamReader(File) 'Legge ogni singola riga del file, fintanto che non si è 'raggiunta la fine Do While Not Reader.EndOfStream 'Come Console.Readline, la funzione d'istanza 'ReadLine restituisce una linea di testo dal file Console.WriteLine(Reader.ReadLine)Loop 'Quindi chiude il file Reader.Close()Case "W" Console.WriteLine("Inserire il percorso del file da creare:") File = Console.ReadLineDim WriterAs New IO.StreamWriter(File)Dim LineAs String Console.WriteLine("Immettere il testo del file, " & _ "premere due volte invio per terminare") 'Fa immettere righe di testo fino a quando si termina Do Line = Console.ReadLine 'Come Console.WriteLine, la funzione d'istanza 'WriteLine scrive una linea di testo sul file Writer.WriteLine(Line)Loop While Line <> "" 'Chiude il file Writer.Close()Case Else Console.WriteLine("Comando non valido!")End Select Console.ReadKey()End Sub End Module
Come si è visto, le classi analizzate (e quelle che andremo a vedere tra breve) hanno metodi molti simili a quelli di Console: questo perchè anche la console è uno stream, capace di input e output allo stesso tempo. Per coloro che provengono dal C non sarà difficile richiamare questo concetto, ma per i neofiti della programmazione, fornisco una breve definizione (presa da Wikipedia), più generale, di stream.
Stream
In programmazione, il termine stream, talvolta tradotto con flusso, si riferisce a una rappresentazione astratta di un flusso
di input/output nell'API di un linguaggio di programmazioneCon l'acronimo API ci si riferisce all'Application Programming Interface di un linguaggio di programmazione, ossia l'insieme di tutti i metodi resi disponibili al programmatore. Globalmente parlando, quindi, si può associare uno stream al flusso di dati proveniente da un qualsiasi dispositivo virtuale o fisico o da qualunque entità astratta all'interno della macchina: ad esempio è possibile avere uno stream associato a una stampante, a uno scanner, allo schermo, ad un file, alla memoria temporanea, a qualsiasi altra cosa. Questo concetto verrà espresso meglio nel capitolo 84, parlando della comunicazione di due computer in rete, gestita attraverso uno stream condiviso. Tuttavia, il .Net Framework non mette a disposizione tutta questa potenza alla classe Stream, ma piuttosto la suddivide in più classi differenti, così abbiamo FileStream, Console, NetworkStream, MemoryStream e via dicendo.
Lettura/scrittura di file binari
Come già accennato nel paragrafo precedente, la distinzione tra file binari e testuali avviene tramite l'interpretazione dei singoli bytes.
Con questo tipo di file, c'è una corrispondenza biunivoca tra i bytes del file e i dati letti dal programma: infatti, non a caso, l'I/O
viene gestito attraverso un array di byte (e qui mi riferisco proprio alle variabili di tipo Byte), denominato Buffer. Il Buffer, di
solito, ha la stessa grandezza del file, o una dimensione prefissata in caso di file estremamente grandi (che vengono letti poco per volta).
BinaryWriter e BinaryReader espongono procedure d'istanza simili a quelle viste prima: Write e Read. Entrambe accettano tre parametri: il
buffer di lettura/scrittura, l'indice da cui iniziare a leggere/scrivere, il numero di bytes da leggere/scrivere. Nel caso di Read il primo
argomento è passato per indirizzo e quindi, dopo la chiamata, contiene i bytes letti. Ecco un esempio di un semplice Hex Editor, con la
sola funzionalità di lettura:
Provando, ad esempio, con un file contenente questo testo:Module Module1Sub Main()Dim ReaderAs IO.BinaryReaderDim Buffer()As Byte Dim FileAs String Console.WriteLine("Inserire il nome del file da leggere:") File = Console.ReadLineIf Not IO.File.Exists(File)Then Console.WriteLine("File inesistente!") Console.ReadKey()Exit Sub End If 'Inizializza lo stream: il costruttore di BinaryReader e 'BinaryWriter è particolare: accetta un parametro di tipo 'Stream. Dato che Stream è una classe astratta, bisogna prima 'creare un nuovo oggetto FileStream con modalità di apertura 'e poi passarlo al costruttore Dim TempAs New IO.FileStream(File, IO.FileMode.Open) Reader =New IO.BinaryReader(Temp) 'Ridimensiona Buffer sulla base delle dimensioni del file ReDim Buffer(Reader.BaseStream.Length) 'Legge tutto il contenuto Reader.Read(Buffer, 0, Reader.BaseStream.Length) Reader.Close() 'Itera su ogni bytes, a gruppetti di 16, e li scrive a schermo For IAs Int32 = 0To Buffer.Length - 1 'La funzione statica String.Format accetta una stringa di 'formato e restituisce una stringa costruita sulla sua base, 'un pò come avviene in Console.WriteLine. In questo caso 'il primo argomento (0) deve essere convertito in base 'esadeciamle (X) ed essere sempre composto da due cifre (2). 'Se l'argomento è minore di 10 (ossia 16 in decimale) 'verranno aggiunto zeri sulla sinistra Console.Write(String .Format("{0:X2} ", Buffer(I)))If (I + 1)Mod 16 = 0Then Console.WriteLine()End If Next Console.ReadKey()End Sub End Module
Questo file sarà analizzato da un programma in grado di evidenziare i singoli bytes in codifica esadecimale.Si otterrà questo output:
51 75 65 73 74 6F 20 66 69 6C 65 20 73 61 72 E0 20 61 6E 61 6C 69 7A 7A 61 74 6F 20 64 61 20 75 6E 20 70 72 6F 67 72 61 6D 6D 61 20 69 6E 20 67 72 61 64 6F 20 64 69 0D 0A 65 76 69 64 65 6E 7A 69 61 72 65 20 69 20 73 69 6E 67 6F 6C 69 20 62 79 74 65 73 20 69 6E 20 63 6F 64 69 66 69 63 61 20 65 73 61 64 65 63 69 6D 61 6C 65 2E 00Con questo programma si possono analizzare i singoli bytes. Ne ho evidenziato qualcuno come dimostrazione. I primi in grassetto sono 0D 0A, che, convertiti in base decimale, sono rispettivamente 13 e 10, ossia, come già detto, i caratteri che costituiscono l'a capo, invisibili nel testo; il secondo è 00 alla fine, ossia il carattere terminatore di stringa che in questo caso termina anche il file. In altri tipi di file 00 può anche trovarsi all'interno, ma non vi è problema poichè, nella modalità binaria, la scasione continua comunque.
Esempio: steganografia su immagini
La steganografia è l'arte di nascondere del testo all'interno di un qualsiasi tipo di file. Per i più curiosi, mi avventurerò nella scrittura
di un semplicissimo programma di steganografia su immagini, nascondendo del testo al loro interno.Per prima cosa, si costruisca l'interfaccia grafica, con questi controlli:
- Una Label, Label1, Text = "Inserire il testo da nascondere:"
- Una TextBox, txtText, ScrollBars = Both, MultiLine = True
- Un Button, cmdHide, Text = "Avvia"
- Un Button, cmdRead, Text = "Leggi da"
Steganografia su immagini
Ed ecco il codice ampiamente commentato:
Il testo accodato può essere rilevato facilmente con un Hex Editor, per questo lo si dovrebbe criptare con una password: per ulteriori informazioni sulla criptazione in .Net, vedere capitolo 91. Per il resto funziona egregiamente: provare per credere! Potete scaricare questa immagine e verificare il testo steganografato all'interno con l'applicazione appena creata.Class Form1Private Sub cmdHide_Click(ByVal senderAs System.Object, _ByVal eAs System.EventArgs)Handles cmdHide.ClickDim OpenAs New OpenFileDialog Open.Filter = "Immagini JPEG|*.jpg"If Open.ShowDialog = Windows.Forms.DialogResult.OKThen Dim PathAs String = Open.FileName 'La funzione ReadAllBytes è molto più veloce: 'legge e restituisce in un sol colpo tutti i bytes di un file Dim Buffer()As Byte = IO.File.ReadAllBytes(Path) 'Crea un nuovo nome per il file steganografato, aggiungendo 'una stringa "_st" alla fine del nome Path = Path.Insert(Path.LastIndexOf("."), "_st") 'Un file JPEG finisce sempre con i bytes FF D9. Dopo si può 'scrivere qualsiasi cosa, non influirà minimamente sulla 'lettura dell'immagine da un qualsiasi editor. 'Questo buffer contiene il testo convertito in bytes Dim TextBuffer()As Byte = _ System.Text.ASCIIEncoding.ASCII.GetBytes(txtText.Text) 'Ora bisogna ampliare il buffer precedente per farci stare 'anche il testo da nascondere Dim NewBuffer(Buffer.Length + TextBuffer.Length - 1)As Byte 'Usando la funzione statica Copy, si copia il Buffer 'precedente all'inizio del nuovo buffer '* Copia Buffer.Length bytes da Buffer e li deposita in 'NewBuffer Array.Copy(Buffer, NewBuffer, Buffer.Length) 'Usando un overload della stessa funzione, si copia 'TextBuffer alla fine del nuovo buffer '* Copia tutto TextBuffer.Length bytes da TextBuffer a 'partire dalla posizione 0 in NewBuffer, partendo dalla 'posizione Buffer.Length, ossia il primo byte disponibile Array.Copy(TextBuffer, 0, NewBuffer, Buffer.Length, _ TextBuffer.Length) 'Ora che si disponde di tutto il necessario, basta scrivere 'i nuovi bytes su un altro file IO.File.WriteAllBytes(Path, NewBuffer) MessageBox.Show("Operazione completata!", "Steganografia", _ MessageBoxButtons.OK)End If End Sub Private Sub cmdRead_Click(ByVal senderAs System.Object, _ByVal eAs System.EventArgs)Handles cmdRead.ClickDim OpenAs New OpenFileDialog Open.Filter = "Immagini JPEG|*.jpg"If Open.ShowDialog = Windows.Forms.DialogResult.OKThen Dim Buffer()As Byte = IO.File.ReadAllBytes(Open.FileName) 'Determina se si è raggiunta la fine del file Dim EOFAs Boolean =False 'Costruisce a poco a poco la stringa nascosta Dim HiddenBytesAs New List(Of Byte ) 'Prima bisogna trovare la sequenza FF D9, corrispondente 'al decimale 255 217 For IAs Int64 = 0To Buffer.Length - 1 'Questo byte fa parte del testo nasosto, quindi... If EOFThen 'Lo aggiunge alla lista HiddenBytes.Add(Buffer(I)) 'E salta il controllo successivo Continue For End If 'Questa istruzione viene eseguita solo se EOF = False 'e quando I è uguale all'ultimo indice dell'array 'Significa che non c'è nessun testo nascosto, perciò 'esce ond'evitare un errore nell'espressione 'Buffer(I+1) della riga successiva, poichè I+1 'non esiste If I = Buffer.Length - 1Then Exit For End If If Buffer(I) = 255And Buffer(I + 1) = 217Then 'Fine del file JPEG: se c'è dell'altro testo, verrà 'accodato mediante HiddenBytes nell'If precedente EOF =True 'Avanza di un byte, poichè c'è ancora D9 che 'non è stato analizzato I += 1End If Next 'Copia la lista generic in un array di bytes, così da 'poterlo convertire in stringa Dim Bytes(HiddenBytes.Count - 1)As Byte HiddenBytes.CopyTo(Bytes) 'Converte i bytes in stringa e li visualizza Dim HiddenString = _ System.Text.ASCIIEncoding.ASCII.GetString(Bytes) txtText.Text = HiddenStringEnd If End Sub End Class
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



