@@ -163,18 +163,43 @@ func readFromFile(filePath string, obj any, encryptionKey string) error {
163163 }
164164 }
165165
166+ r , err := os .Open (filePath )
167+ if err != nil {
168+ return fmt .Errorf ("couldn't open file: %w" , err )
169+ }
170+ defer r .Close ()
171+
172+ return readFromReader (r , obj , encryptionKey )
173+ }
174+
175+ // readFromReader reads an object from a Reader. The object is deserialized from gob.
176+ // `obj` must be a pointer to an instantiated object. The stream may optionally
177+ // be compressed as gzip and/or encrypted with AES-GCM. The encryption key must
178+ // be 32 bytes long.
179+ // If the reader has to be closed, it's the caller's responsibility.
180+ func readFromReader (r io.ReadSeeker , obj any , encryptionKey string ) error {
181+ // AES 256 requires a 32 byte key
182+ if encryptionKey != "" {
183+ if len (encryptionKey ) != 32 {
184+ return errors .New ("encryption key must be 32 bytes long" )
185+ }
186+ }
187+
166188 // We want to:
167- // Read file -> decrypt with AES-GCM -> decompress with flate -> decode as gob
189+ // Read from reader -> decrypt with AES-GCM -> decompress with flate -> decode
190+ // as gob.
168191 // To reduce memory usage we chain the readers instead of buffering, so we start
169192 // from the end. For the decryption there's no reader though.
170193
171- var r io.Reader
194+ // For the chainedReader we don't declare it as ReadSeeker so we can reassign
195+ // the gzip reader to it.
196+ var chainedReader io.Reader
172197
173198 // Decrypt if an encryption key is provided
174199 if encryptionKey != "" {
175- encrypted , err := os . ReadFile ( filePath )
200+ encrypted , err := io . ReadAll ( r )
176201 if err != nil {
177- return fmt .Errorf ("couldn't read file : %w" , err )
202+ return fmt .Errorf ("couldn't read from reader : %w" , err )
178203 }
179204 block , err := aes .NewCipher ([]byte (encryptionKey ))
180205 if err != nil {
@@ -194,28 +219,24 @@ func readFromFile(filePath string, obj any, encryptionKey string) error {
194219 return fmt .Errorf ("couldn't decrypt data: %w" , err )
195220 }
196221
197- r = bytes .NewReader (data )
222+ chainedReader = bytes .NewReader (data )
198223 } else {
199- var err error
200- r , err = os .Open (filePath )
201- if err != nil {
202- return fmt .Errorf ("couldn't open file: %w" , err )
203- }
224+ chainedReader = r
204225 }
205226
206- // Determine if the file is compressed
227+ // Determine if the stream is compressed
207228 magicNumber := make ([]byte , 2 )
208- _ , err := r .Read (magicNumber )
229+ _ , err := chainedReader .Read (magicNumber )
209230 if err != nil {
210- return fmt .Errorf ("couldn't read magic number to determine whether the file is compressed: %w" , err )
231+ return fmt .Errorf ("couldn't read magic number to determine whether the stream is compressed: %w" , err )
211232 }
212233 var compressed bool
213234 if magicNumber [0 ] == 0x1f && magicNumber [1 ] == 0x8b {
214235 compressed = true
215236 }
216237
217- // Reset reader. Both file and bytes.Reader support seeking.
218- if s , ok := r .(io.Seeker ); ! ok {
238+ // Reset reader. Both the reader from the param and bytes.Reader support seeking.
239+ if s , ok := chainedReader .(io.Seeker ); ! ok {
219240 return fmt .Errorf ("reader doesn't support seeking" )
220241 } else {
221242 _ , err := s .Seek (0 , 0 )
@@ -225,15 +246,15 @@ func readFromFile(filePath string, obj any, encryptionKey string) error {
225246 }
226247
227248 if compressed {
228- gzr , err := gzip .NewReader (r )
249+ gzr , err := gzip .NewReader (chainedReader )
229250 if err != nil {
230251 return fmt .Errorf ("couldn't create gzip reader: %w" , err )
231252 }
232253 defer gzr .Close ()
233- r = gzr
254+ chainedReader = gzr
234255 }
235256
236- dec := gob .NewDecoder (r )
257+ dec := gob .NewDecoder (chainedReader )
237258 err = dec .Decode (obj )
238259 if err != nil {
239260 return fmt .Errorf ("couldn't decode object: %w" , err )
0 commit comments