keramics_formats/ext/
file_entry.rs

1/* Copyright 2024-2025 Joachim Metz <joachim.metz@gmail.com>
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may
5 * obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software
8 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 * License for the specific language governing permissions and limitations
11 * under the License.
12 */
13
14use std::cmp::max;
15use std::collections::BTreeMap;
16use std::sync::{Arc, RwLock};
17
18use keramics_core::{DataStream, DataStreamReference, ErrorTrace, FakeDataStream};
19use keramics_datetime::DateTime;
20use keramics_types::{ByteString, bytes_to_u16_le};
21
22use super::block_stream::ExtBlockStream;
23use super::constants::*;
24use super::directory_entry::ExtDirectoryEntry;
25use super::directory_tree::ExtDirectoryTree;
26use super::inode::ExtInode;
27use super::inode_table::ExtInodeTable;
28
29/// Extended File System (ext) file entry.
30pub struct ExtFileEntry {
31    /// The data stream.
32    data_stream: DataStreamReference,
33
34    /// Inode table.
35    inode_table: Arc<ExtInodeTable>,
36
37    /// The inode number.
38    pub inode_number: u32,
39
40    /// The inode.
41    inode: ExtInode,
42
43    /// The name.
44    name: Option<ByteString>,
45
46    /// Directory entries.
47    directory_entries: BTreeMap<ByteString, ExtDirectoryEntry>,
48
49    /// Value to indicate the directory entries were read.
50    read_directory_entries: bool,
51
52    /// Symbolic link target.
53    symbolic_link_target: Option<ByteString>,
54}
55
56impl ExtFileEntry {
57    /// Creates a new file entry.
58    pub(super) fn new(
59        data_stream: &DataStreamReference,
60        inode_table: &Arc<ExtInodeTable>,
61        inode_number: u32,
62        inode: ExtInode,
63        name: Option<ByteString>,
64    ) -> Self {
65        Self {
66            data_stream: data_stream.clone(),
67            inode_table: inode_table.clone(),
68            inode_number: inode_number,
69            inode: inode,
70            name: name,
71            directory_entries: BTreeMap::new(),
72            read_directory_entries: false,
73            symbolic_link_target: None,
74        }
75    }
76
77    /// Retrieves the access time.
78    pub fn get_access_time(&self) -> Option<&DateTime> {
79        Some(&self.inode.access_time)
80    }
81
82    /// Retrieves the change time.
83    pub fn get_change_time(&self) -> Option<&DateTime> {
84        Some(&self.inode.change_time)
85    }
86
87    /// Retrieves the creation time.
88    pub fn get_creation_time(&self) -> Option<&DateTime> {
89        self.inode.creation_time.as_ref()
90    }
91
92    /// Retrieves the deletion time.
93    pub fn get_deletion_time(&self) -> &DateTime {
94        &self.inode.deletion_time
95    }
96
97    /// Retrieves the device identifier.
98    pub fn get_device_identifier(&mut self) -> Result<Option<u16>, ErrorTrace> {
99        if self.inode.file_mode & 0xf000 == EXT_FILE_MODE_TYPE_CHARACTER_DEVICE
100            || self.inode.file_mode & 0xf000 == EXT_FILE_MODE_TYPE_BLOCK_DEVICE
101        {
102            if self.inode.data_size > 2 {
103                return Err(keramics_core::error_trace_new!(format!(
104                    "Invalid device identifier data size: {} value out of bounds",
105                    self.inode.data_size,
106                )));
107            }
108            let device_identifier: u16 = bytes_to_u16_le!(&self.inode.data_reference, 0);
109            return Ok(Some(device_identifier));
110        }
111        Ok(None)
112    }
113
114    /// Retrieves the file mode.
115    pub fn get_file_mode(&self) -> u16 {
116        self.inode.file_mode
117    }
118
119    /// Retrieves the group identifier.
120    pub fn get_group_identifier(&self) -> u32 {
121        self.inode.group_identifier
122    }
123
124    /// Retrieves the modification time.
125    pub fn get_modification_time(&self) -> Option<&DateTime> {
126        Some(&self.inode.modification_time)
127    }
128
129    /// Retrieves the name.
130    pub fn get_name(&self) -> Option<&ByteString> {
131        self.name.as_ref()
132    }
133
134    /// Retrieves the number of links.
135    pub fn get_number_of_links(&self) -> u16 {
136        self.inode.number_of_links
137    }
138
139    /// Retrieves the owner identifier.
140    pub fn get_owner_identifier(&self) -> u32 {
141        self.inode.owner_identifier
142    }
143
144    /// Retrieves the size.
145    pub fn get_size(&self) -> u64 {
146        match self.inode.file_mode & 0xf000 {
147            EXT_FILE_MODE_TYPE_REGULAR_FILE | EXT_FILE_MODE_TYPE_SYMBOLIC_LINK => {
148                self.inode.data_size
149            }
150            _ => 0,
151        }
152    }
153
154    /// Retrieves the symbolic link target.
155    pub fn get_symbolic_link_target(&mut self) -> Result<Option<&ByteString>, ErrorTrace> {
156        if self.symbolic_link_target.is_none()
157            && self.inode.file_mode & 0xf000 == EXT_FILE_MODE_TYPE_SYMBOLIC_LINK
158        {
159            if self.inode.data_size > (self.inode_table.block_size as u64) {
160                return Err(keramics_core::error_trace_new!(format!(
161                    "Invalid symbolic link target data size: {} value out of bounds",
162                    self.inode.data_size,
163                )));
164            }
165            let byte_string: ByteString = if self.inode.data_size < 60 {
166                ByteString::from(self.inode.data_reference.as_slice())
167            } else {
168                let number_of_blocks: u64 = max(
169                    self.inode
170                        .data_size
171                        .div_ceil(self.inode_table.block_size as u64),
172                    self.inode.number_of_blocks,
173                );
174                let mut block_stream: ExtBlockStream =
175                    ExtBlockStream::new(self.inode_table.block_size, self.inode.data_size);
176
177                match block_stream.open(
178                    &self.data_stream,
179                    number_of_blocks,
180                    &self.inode.block_ranges,
181                ) {
182                    Ok(_) => {}
183                    Err(mut error) => {
184                        keramics_core::error_trace_add_frame!(error, "Unable to open block stream");
185                        return Err(error);
186                    }
187                }
188                let mut data: Vec<u8> = vec![0; self.inode.data_size as usize];
189
190                match block_stream.read_exact(&mut data) {
191                    Ok(_) => {}
192                    Err(mut error) => {
193                        keramics_core::error_trace_add_frame!(
194                            error,
195                            "Unable to read symbolic link target data from block stream"
196                        );
197                        return Err(error);
198                    }
199                }
200                ByteString::from(&data)
201            };
202            self.symbolic_link_target = Some(byte_string);
203        }
204        Ok(self.symbolic_link_target.as_ref())
205    }
206
207    /// Retrieves the number of attributes.
208    pub fn get_number_of_attributes(&mut self) -> Result<usize, ErrorTrace> {
209        Ok(self.inode.attributes.len())
210    }
211
212    // TODO: add get extended_attributes iterator
213
214    /// Retrieves the default data stream.
215    pub fn get_data_stream(&self) -> Result<Option<DataStreamReference>, ErrorTrace> {
216        if self.inode.file_mode & 0xf000 != EXT_FILE_MODE_TYPE_REGULAR_FILE {
217            return Ok(None);
218        }
219        if self.inode.flags & EXT_INODE_FLAG_INLINE_DATA != 0 {
220            let data_stream: FakeDataStream =
221                FakeDataStream::new(&self.inode.data_reference, self.inode.data_size);
222            Ok(Some(Arc::new(RwLock::new(data_stream))))
223        } else {
224            let number_of_blocks: u64 = max(
225                self.inode
226                    .data_size
227                    .div_ceil(self.inode_table.block_size as u64),
228                self.inode.number_of_blocks,
229            );
230            let mut block_stream: ExtBlockStream =
231                ExtBlockStream::new(self.inode_table.block_size, self.inode.data_size);
232
233            match block_stream.open(
234                &self.data_stream,
235                number_of_blocks,
236                &self.inode.block_ranges,
237            ) {
238                Ok(_) => {}
239                Err(mut error) => {
240                    keramics_core::error_trace_add_frame!(error, "Unable to open block stream");
241                    return Err(error);
242                }
243            }
244            Ok(Some(Arc::new(RwLock::new(block_stream))))
245        }
246    }
247
248    /// Retrieves the number of sub file entries.
249    pub fn get_number_of_sub_file_entries(&mut self) -> Result<usize, ErrorTrace> {
250        if !self.read_directory_entries {
251            match self.read_directory_entries() {
252                Ok(_) => {}
253                Err(mut error) => {
254                    keramics_core::error_trace_add_frame!(
255                        error,
256                        "Unable to read directory entries"
257                    );
258                    return Err(error);
259                }
260            }
261        }
262        Ok(self.directory_entries.len())
263    }
264
265    /// Retrieves a specific sub file entry.
266    pub fn get_sub_file_entry_by_index(
267        &mut self,
268        sub_file_entry_index: usize,
269    ) -> Result<ExtFileEntry, ErrorTrace> {
270        if !self.read_directory_entries {
271            match self.read_directory_entries() {
272                Ok(_) => {}
273                Err(mut error) => {
274                    keramics_core::error_trace_add_frame!(
275                        error,
276                        "Unable to read directory entries"
277                    );
278                    return Err(error);
279                }
280            }
281        }
282        let (name, directory_entry): (&ByteString, &ExtDirectoryEntry) =
283            match self.directory_entries.iter().nth(sub_file_entry_index) {
284                Some(key_and_value) => key_and_value,
285                None => {
286                    return Err(keramics_core::error_trace_new!(format!(
287                        "Missing directory entry: {}",
288                        sub_file_entry_index
289                    )));
290                }
291            };
292        let inode: ExtInode = match self
293            .inode_table
294            .get_inode(&self.data_stream, directory_entry.inode_number)
295        {
296            Ok(inode) => inode,
297            Err(mut error) => {
298                keramics_core::error_trace_add_frame!(
299                    error,
300                    format!("Unable to retrieve inode: {}", directory_entry.inode_number)
301                );
302                return Err(error);
303            }
304        };
305        let file_entry: ExtFileEntry = ExtFileEntry::new(
306            &self.data_stream,
307            &self.inode_table,
308            directory_entry.inode_number,
309            inode,
310            Some(name.clone()),
311        );
312        Ok(file_entry)
313    }
314
315    // TODO: add get_sub_file_entries iterator
316
317    /// Retrieves a specific sub file entry.
318    pub fn get_sub_file_entry_by_name(
319        &mut self,
320        sub_file_entry_name: &ByteString,
321    ) -> Result<Option<ExtFileEntry>, ErrorTrace> {
322        if !self.read_directory_entries {
323            match self.read_directory_entries() {
324                Ok(_) => {}
325                Err(mut error) => {
326                    keramics_core::error_trace_add_frame!(
327                        error,
328                        "Unable to read directory entries"
329                    );
330                    return Err(error);
331                }
332            }
333        }
334        let (name, directory_entry): (&ByteString, &ExtDirectoryEntry) =
335            match self.directory_entries.get_key_value(sub_file_entry_name) {
336                Some(key_and_value) => key_and_value,
337                None => return Ok(None),
338            };
339        let inode: ExtInode = match self
340            .inode_table
341            .get_inode(&self.data_stream, directory_entry.inode_number)
342        {
343            Ok(inode) => inode,
344            Err(mut error) => {
345                keramics_core::error_trace_add_frame!(
346                    error,
347                    format!("Unable to retrieve inode: {}", directory_entry.inode_number)
348                );
349                return Err(error);
350            }
351        };
352        let file_entry: ExtFileEntry = ExtFileEntry::new(
353            &self.data_stream,
354            &self.inode_table,
355            directory_entry.inode_number,
356            inode,
357            Some(name.clone()),
358        );
359        Ok(Some(file_entry))
360    }
361
362    /// Determines if the file entry is the root directory.
363    pub fn is_root_directory(&self) -> bool {
364        self.inode_number == EXT_ROOT_DIRECTORY_IDENTIFIER
365    }
366
367    /// Reads the directory entries.
368    fn read_directory_entries(&mut self) -> Result<(), ErrorTrace> {
369        if self.inode.file_mode & 0xf000 == EXT_FILE_MODE_TYPE_DIRECTORY {
370            let mut directory_tree: ExtDirectoryTree =
371                ExtDirectoryTree::new(self.inode_table.block_size);
372
373            if self.inode.flags & EXT_INODE_FLAG_INLINE_DATA != 0 {
374                match directory_tree
375                    .read_inline_data(&self.inode.data_reference, &mut self.directory_entries)
376                {
377                    Ok(_) => {}
378                    Err(mut error) => {
379                        keramics_core::error_trace_add_frame!(
380                            error,
381                            "Unable to read directory entries from inline data"
382                        );
383                        return Err(error);
384                    }
385                }
386            } else {
387                match directory_tree.read_block_data(
388                    &self.data_stream,
389                    &self.inode.block_ranges,
390                    &mut self.directory_entries,
391                ) {
392                    Ok(_) => {}
393                    Err(mut error) => {
394                        keramics_core::error_trace_add_frame!(
395                            error,
396                            "Unable to read directory entries"
397                        );
398                        return Err(error);
399                    }
400                }
401            }
402        }
403        self.read_directory_entries = true;
404
405        Ok(())
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    use std::path::PathBuf;
414
415    use keramics_core::open_os_data_stream;
416    use keramics_datetime::PosixTime32;
417
418    use crate::ext::file_system::ExtFileSystem;
419    use crate::ext::path::ExtPath;
420
421    fn get_file_system() -> Result<ExtFileSystem, ErrorTrace> {
422        let mut file_system: ExtFileSystem = ExtFileSystem::new();
423
424        let path_buf: PathBuf = PathBuf::from("../test_data/ext/ext2.raw");
425        let data_stream: DataStreamReference = open_os_data_stream(&path_buf)?;
426        file_system.read_data_stream(&data_stream)?;
427
428        Ok(file_system)
429    }
430
431    #[test]
432    fn test_get_access_time() -> Result<(), ErrorTrace> {
433        let ext_file_system: ExtFileSystem = get_file_system()?;
434
435        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
436        let ext_file_entry: ExtFileEntry =
437            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
438
439        assert_eq!(
440            ext_file_entry.get_access_time(),
441            Some(&DateTime::PosixTime32(PosixTime32 {
442                timestamp: 1735977482
443            }))
444        );
445
446        Ok(())
447    }
448
449    #[test]
450    fn test_get_change_time() -> Result<(), ErrorTrace> {
451        let ext_file_system: ExtFileSystem = get_file_system()?;
452
453        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
454        let ext_file_entry: ExtFileEntry =
455            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
456
457        assert_eq!(
458            ext_file_entry.get_change_time(),
459            Some(&DateTime::PosixTime32(PosixTime32 {
460                timestamp: 1735977481
461            }))
462        );
463
464        Ok(())
465    }
466
467    #[test]
468    fn test_get_creation_time() -> Result<(), ErrorTrace> {
469        let ext_file_system: ExtFileSystem = get_file_system()?;
470
471        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
472        let ext_file_entry: ExtFileEntry =
473            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
474
475        assert_eq!(ext_file_entry.get_creation_time(), None);
476
477        Ok(())
478    }
479
480    #[test]
481    fn test_get_device_identifier() -> Result<(), ErrorTrace> {
482        let ext_file_system: ExtFileSystem = get_file_system()?;
483
484        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
485        let mut ext_file_entry: ExtFileEntry =
486            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
487
488        let device_identifier: Option<u16> = ext_file_entry.get_device_identifier()?;
489        assert_eq!(device_identifier, None);
490
491        // TODO: test with block or character device file entry
492
493        Ok(())
494    }
495
496    #[test]
497    fn test_get_file_mode() -> Result<(), ErrorTrace> {
498        let ext_file_system: ExtFileSystem = get_file_system()?;
499
500        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
501        let ext_file_entry: ExtFileEntry =
502            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
503
504        assert_eq!(ext_file_entry.get_file_mode(), 0o100644);
505
506        Ok(())
507    }
508
509    #[test]
510    fn test_get_group_identifier() -> Result<(), ErrorTrace> {
511        let ext_file_system: ExtFileSystem = get_file_system()?;
512
513        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
514        let ext_file_entry: ExtFileEntry =
515            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
516
517        assert_eq!(ext_file_entry.get_group_identifier(), 1000);
518
519        Ok(())
520    }
521
522    #[test]
523    fn test_get_deletion_time() -> Result<(), ErrorTrace> {
524        let ext_file_system: ExtFileSystem = get_file_system()?;
525
526        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
527        let ext_file_entry: ExtFileEntry =
528            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
529
530        assert_eq!(ext_file_entry.get_deletion_time(), &DateTime::NotSet);
531
532        Ok(())
533    }
534
535    #[test]
536    fn test_get_modification_time() -> Result<(), ErrorTrace> {
537        let ext_file_system: ExtFileSystem = get_file_system()?;
538
539        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
540        let ext_file_entry: ExtFileEntry =
541            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
542
543        assert_eq!(
544            ext_file_entry.get_modification_time(),
545            Some(&DateTime::PosixTime32(PosixTime32 {
546                timestamp: 1735977481
547            }))
548        );
549
550        Ok(())
551    }
552
553    #[test]
554    fn test_get_name() -> Result<(), ErrorTrace> {
555        let ext_file_system: ExtFileSystem = get_file_system()?;
556
557        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
558        let ext_file_entry: ExtFileEntry =
559            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
560
561        let name: &ByteString = ext_file_entry.get_name().unwrap();
562        assert_eq!(name.to_string(), "testfile1");
563
564        Ok(())
565    }
566
567    #[test]
568    fn test_get_number_of_links() -> Result<(), ErrorTrace> {
569        let ext_file_system: ExtFileSystem = get_file_system()?;
570
571        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
572        let ext_file_entry: ExtFileEntry =
573            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
574
575        assert_eq!(ext_file_entry.get_number_of_links(), 2);
576
577        Ok(())
578    }
579
580    #[test]
581    fn test_get_owner_identifier() -> Result<(), ErrorTrace> {
582        let ext_file_system: ExtFileSystem = get_file_system()?;
583
584        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
585        let ext_file_entry: ExtFileEntry =
586            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
587
588        assert_eq!(ext_file_entry.get_owner_identifier(), 1000);
589
590        Ok(())
591    }
592
593    #[test]
594    fn test_get_size() -> Result<(), ErrorTrace> {
595        let ext_file_system: ExtFileSystem = get_file_system()?;
596
597        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
598        let ext_file_entry: ExtFileEntry =
599            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
600
601        assert_eq!(ext_file_entry.get_size(), 9);
602
603        Ok(())
604    }
605
606    #[test]
607    fn test_get_symbolic_link_target() -> Result<(), ErrorTrace> {
608        let ext_file_system: ExtFileSystem = get_file_system()?;
609
610        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
611        let mut ext_file_entry: ExtFileEntry =
612            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
613
614        let symbolic_link_target: Option<&ByteString> =
615            ext_file_entry.get_symbolic_link_target()?;
616        assert_eq!(symbolic_link_target, None);
617
618        // TODO: test with symbolic link file entry
619
620        Ok(())
621    }
622
623    #[test]
624    fn test_get_data_stream() -> Result<(), ErrorTrace> {
625        let ext_file_system: ExtFileSystem = get_file_system()?;
626
627        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
628        let ext_file_entry: ExtFileEntry =
629            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
630
631        ext_file_entry.get_data_stream()?;
632
633        Ok(())
634    }
635
636    #[test]
637    fn test_get_number_of_attributes() -> Result<(), ErrorTrace> {
638        let ext_file_system: ExtFileSystem = get_file_system()?;
639
640        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
641        let mut ext_file_entry: ExtFileEntry =
642            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
643
644        let number_of_attributes: usize = ext_file_entry.get_number_of_attributes()?;
645        assert_eq!(number_of_attributes, 0);
646
647        // TODO: test with file entry with attributes
648
649        Ok(())
650    }
651
652    #[test]
653    fn test_get_number_of_sub_file_entries() -> Result<(), ErrorTrace> {
654        let ext_file_system: ExtFileSystem = get_file_system()?;
655
656        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
657        let mut ext_file_entry: ExtFileEntry =
658            ext_file_system.get_file_entry_by_path(&ext_path)?.unwrap();
659
660        let number_of_sub_file_entries: usize = ext_file_entry.get_number_of_sub_file_entries()?;
661        assert_eq!(number_of_sub_file_entries, 0);
662
663        // TODO: test with directory file entry
664
665        Ok(())
666    }
667
668    // TODO: add tests for get_sub_file_entry_by_index
669    // TODO: add tests for get_sub_file_entry_by_name
670
671    // TODO: add tests for is_root_directory
672
673    // TODO: add tests for read_directory_entries
674}