keramics_formats/ext/
file_system.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::io::SeekFrom;
15use std::sync::Arc;
16
17use keramics_checksums::ReversedCrc32Context;
18use keramics_core::{DataStreamReference, ErrorTrace};
19use keramics_datetime::DateTime;
20use keramics_types::ByteString;
21
22use super::constants::*;
23use super::features::ExtFeatures;
24use super::file_entry::ExtFileEntry;
25use super::group_descriptor::ExtGroupDescriptor;
26use super::group_descriptor_table::ExtGroupDescriptorTable;
27use super::inode::ExtInode;
28use super::inode_table::ExtInodeTable;
29use super::path::ExtPath;
30use super::superblock::ExtSuperblock;
31
32/// Extended File System (ext).
33pub struct ExtFileSystem {
34    /// Data stream.
35    data_stream: Option<DataStreamReference>,
36
37    /// Features.
38    features: ExtFeatures,
39
40    /// Number of inodes.
41    pub number_of_inodes: u32,
42
43    /// Block size.
44    block_size: u32,
45
46    /// Inode size.
47    inode_size: u16,
48
49    /// Inode table.
50    inode_table: Arc<ExtInodeTable>,
51
52    /// Metadata checksum seed.
53    metadata_checksum_seed: u32,
54
55    /// Volume label.
56    volume_label: ByteString,
57
58    /// Last mount path.
59    pub last_mount_path: ByteString,
60
61    /// Last mount time.
62    pub last_mount_time: DateTime,
63
64    /// Last written time.
65    pub last_written_time: DateTime,
66}
67
68impl ExtFileSystem {
69    /// Creates a new file system.
70    pub fn new() -> Self {
71        Self {
72            data_stream: None,
73            volume_label: ByteString::new(),
74            features: ExtFeatures::new(),
75            number_of_inodes: 0,
76            block_size: 0,
77            inode_size: 0,
78            inode_table: Arc::new(ExtInodeTable::new()),
79            metadata_checksum_seed: 0,
80            last_mount_path: ByteString::new(),
81            last_mount_time: DateTime::NotSet,
82            last_written_time: DateTime::NotSet,
83        }
84    }
85
86    /// Retrieves the format version.
87    pub fn get_format_version(&self) -> u8 {
88        self.features.get_format_version()
89    }
90
91    /// Retrieves the compatible feature flags.
92    pub fn get_compatible_feature_flags(&self) -> u32 {
93        self.features.compatible_feature_flags
94    }
95
96    /// Retrieves the incompatible feature flags.
97    pub fn get_incompatible_feature_flags(&self) -> u32 {
98        self.features.incompatible_feature_flags
99    }
100
101    /// Retrieves the read-only compatible feature flags.
102    pub fn get_read_only_compatible_feature_flags(&self) -> u32 {
103        self.features.read_only_compatible_feature_flags
104    }
105
106    /// Retrieves the volume label.
107    pub fn get_volume_label(&self) -> &ByteString {
108        &self.volume_label
109    }
110
111    /// Retrieves the file entry for a specific identifier (inode number).
112    pub fn get_file_entry_by_identifier(
113        &self,
114        inode_number: u32,
115    ) -> Result<ExtFileEntry, ErrorTrace> {
116        let data_stream: &DataStreamReference = match self.data_stream.as_ref() {
117            Some(data_stream) => data_stream,
118            None => {
119                return Err(keramics_core::error_trace_new!("Missing data stream"));
120            }
121        };
122        if self.features.is_unsupported() {
123            return Err(keramics_core::error_trace_new!(
124                "Ext file system has unsupported features"
125            ));
126        }
127        if inode_number == 0 || inode_number > self.number_of_inodes {
128            return Err(keramics_core::error_trace_new!(format!(
129                "Invalid inode number: {} value out of bounds",
130                inode_number
131            )));
132        }
133        let inode: ExtInode = match self.inode_table.get_inode(data_stream, inode_number) {
134            Ok(inode) => inode,
135            Err(mut error) => {
136                keramics_core::error_trace_add_frame!(
137                    error,
138                    format!("Unable to retrieve inode: {}", inode_number)
139                );
140                return Err(error);
141            }
142        };
143        Ok(ExtFileEntry::new(
144            data_stream,
145            &self.inode_table,
146            inode_number,
147            inode,
148            None,
149        ))
150    }
151
152    /// Retrieves the file entry for a specific path.
153    pub fn get_file_entry_by_path(
154        &self,
155        path: &ExtPath,
156    ) -> Result<Option<ExtFileEntry>, ErrorTrace> {
157        if path.is_empty() || path.components[0].len() != 0 {
158            return Ok(None);
159        }
160        let result: Option<ExtFileEntry> = match self.get_root_directory() {
161            Ok(result) => result,
162            Err(mut error) => {
163                keramics_core::error_trace_add_frame!(error, "Unable to retrieve root directory");
164                return Err(error);
165            }
166        };
167        let mut file_entry: ExtFileEntry = match result {
168            Some(file_entry) => file_entry,
169            None => return Ok(None),
170        };
171        // TODO: cache file entries.
172        for path_component in path.components[1..].iter() {
173            let result: Option<ExtFileEntry> =
174                match file_entry.get_sub_file_entry_by_name(path_component) {
175                    Ok(result) => result,
176                    Err(mut error) => {
177                        keramics_core::error_trace_add_frame!(
178                            error,
179                            format!(
180                                "Unable to retrieve sub file entry: {}",
181                                path_component.to_string()
182                            )
183                        );
184                        return Err(error);
185                    }
186                };
187            file_entry = match result {
188                Some(file_entry) => file_entry,
189                None => return Ok(None),
190            };
191        }
192        Ok(Some(file_entry))
193    }
194
195    /// Retrieves the root directory (file entry).
196    pub fn get_root_directory(&self) -> Result<Option<ExtFileEntry>, ErrorTrace> {
197        if self.number_of_inodes == 0 {
198            return Ok(None);
199        }
200        match self.get_file_entry_by_identifier(EXT_ROOT_DIRECTORY_IDENTIFIER) {
201            Ok(file_entry) => Ok(Some(file_entry)),
202            Err(mut error) => {
203                keramics_core::error_trace_add_frame!(
204                    error,
205                    format!(
206                        "Unable to retrieve file entry: {}",
207                        EXT_ROOT_DIRECTORY_IDENTIFIER
208                    )
209                );
210                Err(error)
211            }
212        }
213    }
214
215    /// Reads a file system from a data stream.
216    pub fn read_data_stream(
217        &mut self,
218        data_stream: &DataStreamReference,
219    ) -> Result<(), ErrorTrace> {
220        match self.read_block_groups(data_stream) {
221            Ok(_) => {}
222            Err(mut error) => {
223                keramics_core::error_trace_add_frame!(error, "Unable to read block groups");
224                return Err(error);
225            }
226        }
227        self.data_stream = Some(data_stream.clone());
228
229        Ok(())
230    }
231
232    /// Reads the block groups.
233    fn read_block_groups(&mut self, data_stream: &DataStreamReference) -> Result<(), ErrorTrace> {
234        let mut block_group_number: u32 = 0;
235        let mut block_group_offset: u64 = 0;
236        let mut block_group_size: u64 = 0;
237        let mut exponent3: u32 = 3;
238        let mut exponent5: u32 = 5;
239        let mut exponent7: u32 = 7;
240        let mut meta_group_number: u32 = 0;
241        let mut meta_group_start_block_number: u32 = 0;
242        let mut number_of_block_groups: u64 = 0;
243        let mut number_of_block_groups_per_meta_group: u32 = 0;
244        let mut number_of_inodes_per_block_group: u32 = 0;
245        let mut group_descriptors: Vec<ExtGroupDescriptor> = Vec::new();
246
247        loop {
248            if exponent3 < block_group_number {
249                exponent3 *= 3;
250            }
251            if exponent5 < block_group_number {
252                exponent5 *= 5;
253            }
254            if exponent7 < block_group_number {
255                exponent7 *= 7;
256            }
257            // The first block group will always contain a superblock.
258            let block_group_has_superblock: bool = if block_group_number == 0 {
259                true
260            // If sparse superblock feature is disabled all block groups contain a superblock.
261            } else if !self.features.has_sparse_superblock() {
262                true
263            // TODO: add support for sparse superblock version 2
264            } else if self.features.has_sparse_superblock2() {
265                false
266            // Only block group numbers that are a power of 3, 5 or 7 contain a superblock.
267            } else if block_group_number == 1
268                || block_group_number == exponent3
269                || block_group_number == exponent5
270                || block_group_number == exponent7
271            {
272                true
273            } else {
274                false
275            };
276            if block_group_has_superblock {
277                let mut superblock_offset: u64 = block_group_offset;
278
279                if block_group_offset == 0 || self.block_size == 1024 {
280                    superblock_offset += 1024;
281                }
282                if block_group_number == 0 {
283                    let mut superblock: ExtSuperblock = ExtSuperblock::new();
284
285                    match superblock
286                        .read_at_position(data_stream, SeekFrom::Start(superblock_offset))
287                    {
288                        Ok(_) => {}
289                        Err(mut error) => {
290                            keramics_core::error_trace_add_frame!(
291                                error,
292                                format!(
293                                    "Unable to read superblock at offset: {} (0x{:08x})",
294                                    superblock_offset, superblock_offset
295                                )
296                            );
297                            return Err(error);
298                        }
299                    }
300                    self.features.initialize(&superblock);
301
302                    number_of_block_groups = superblock.get_number_of_block_groups();
303                    block_group_size = superblock.get_block_group_size();
304
305                    self.number_of_inodes = superblock.number_of_inodes;
306                    self.block_size = superblock.block_size;
307                    self.inode_size = superblock.inode_size;
308
309                    self.volume_label = superblock.volume_label;
310
311                    // TODO: change to ExtPath
312                    self.last_mount_path = superblock.last_mount_path;
313
314                    if superblock.last_mount_time.timestamp != 0 {
315                        self.last_mount_time = DateTime::PosixTime32(superblock.last_mount_time);
316                    }
317                    if superblock.last_written_time.timestamp != 0 {
318                        self.last_written_time =
319                            DateTime::PosixTime32(superblock.last_written_time);
320                    }
321                    number_of_inodes_per_block_group = superblock.number_of_inodes_per_block_group;
322
323                    if self.features.has_meta_block_groups() {
324                        let group_descriptor_size: u32 = self.features.get_group_descriptor_size();
325                        number_of_block_groups_per_meta_group =
326                            superblock.block_size / group_descriptor_size;
327                        meta_group_start_block_number = superblock.first_meta_block_group
328                            * number_of_block_groups_per_meta_group;
329                    }
330                    self.metadata_checksum_seed = match superblock.metadata_checksum_seed {
331                        Some(metadata_checksum_seed) => metadata_checksum_seed,
332                        None => {
333                            let mut crc32_context: ReversedCrc32Context =
334                                ReversedCrc32Context::new(0x82f63b78, 0);
335                            crc32_context.update(&superblock.file_system_identifier);
336                            crc32_context.finalize()
337                        }
338                    };
339                } else {
340                    let mut superblock: ExtSuperblock = ExtSuperblock::new();
341
342                    match superblock
343                        .read_at_position(data_stream, SeekFrom::Start(superblock_offset))
344                    {
345                        Ok(_) => {
346                            // TODO: compare superblock
347                        }
348                        // Ignore backup superblocks without a correct signature.
349                        Err(_) => {}
350                    }
351                }
352            }
353            // When the has meta block groups feature is enabled group descriptors are stored at the
354            // beginning of the first, second, and last block groups in a meta block group,
355            // independent of a superblock. Otherwise group descriptors are stored in the first
356            // block after a superblock.
357            let mut block_group_has_group_descriptors: bool = false;
358            let mut meta_group_block_group_number: u32 = 0;
359
360            if !self.features.has_meta_block_groups()
361                || block_group_number < meta_group_start_block_number
362            {
363                block_group_has_group_descriptors = block_group_has_superblock;
364            } else {
365                meta_group_block_group_number =
366                    block_group_number % number_of_block_groups_per_meta_group;
367
368                if meta_group_block_group_number == 0
369                    || meta_group_block_group_number == 1
370                    || meta_group_block_group_number == number_of_block_groups_per_meta_group - 1
371                {
372                    block_group_has_group_descriptors = true;
373                }
374            }
375            if block_group_has_group_descriptors {
376                let mut group_descriptor_offset: u64 = block_group_offset;
377
378                if self.block_size == 1024 {
379                    group_descriptor_offset += 1024;
380                }
381                if block_group_has_superblock {
382                    group_descriptor_offset += self.block_size as u64;
383                }
384                let first_group_number: u32 = if !self.features.has_meta_block_groups()
385                    || block_group_number < meta_group_start_block_number
386                {
387                    0
388                } else {
389                    meta_group_start_block_number
390                        + (meta_group_number * number_of_block_groups_per_meta_group)
391                };
392                let number_of_group_descriptors: u32 = if !self.features.has_meta_block_groups() {
393                    number_of_block_groups as u32
394                } else if block_group_number < meta_group_start_block_number {
395                    meta_group_start_block_number
396                } else {
397                    number_of_block_groups_per_meta_group
398                };
399                let mut group_descriptor_table: ExtGroupDescriptorTable =
400                    ExtGroupDescriptorTable::new();
401                group_descriptor_table.initialize(
402                    &self.features,
403                    first_group_number,
404                    number_of_group_descriptors,
405                );
406                match group_descriptor_table
407                    .read_at_position(data_stream, SeekFrom::Start(group_descriptor_offset))
408                {
409                    Ok(_) => {}
410                    Err(mut error) => {
411                        keramics_core::error_trace_add_frame!(
412                            error,
413                            format!(
414                                "Unable to read group descriptor table at offset: {} (0x{:08x})",
415                                group_descriptor_offset, group_descriptor_offset
416                            )
417                        );
418                        return Err(error);
419                    }
420                }
421                if !self.features.has_meta_block_groups()
422                    || block_group_number < meta_group_start_block_number
423                {
424                    if block_group_number == 0 {
425                        for group_descriptor in group_descriptor_table.entries.drain(0..) {
426                            group_descriptors.push(group_descriptor);
427                        }
428                    }
429                } else if meta_group_block_group_number == 0 {
430                    for group_descriptor in group_descriptor_table.entries.drain(0..) {
431                        group_descriptors.push(group_descriptor);
432                    }
433                } else if meta_group_block_group_number == number_of_block_groups_per_meta_group - 1
434                {
435                    meta_group_number += 1;
436                };
437            }
438            // TODO: read block bitmap for debugging purposes
439            // TODO: read inode bitmap for debugging purposes
440
441            block_group_number += 1;
442            block_group_offset += block_group_size;
443
444            if block_group_number as u64 >= number_of_block_groups {
445                break;
446            }
447        }
448        if number_of_inodes_per_block_group > 0 {
449            match Arc::get_mut(&mut self.inode_table) {
450                Some(inode_table) => match inode_table.initialize(
451                    &self.features,
452                    self.block_size,
453                    self.inode_size,
454                    number_of_inodes_per_block_group,
455                    &mut group_descriptors,
456                ) {
457                    Ok(_) => {}
458                    Err(mut error) => {
459                        keramics_core::error_trace_add_frame!(
460                            error,
461                            "Unable to initialize inode table"
462                        );
463                        return Err(error);
464                    }
465                },
466                None => {
467                    return Err(keramics_core::error_trace_new!(
468                        "Unable to obtain mutable reference to inode table"
469                    ));
470                }
471            };
472        }
473        // TODO: sanity check self.number_of_inodes and size of inode table.
474
475        Ok(())
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    use std::path::PathBuf;
484
485    use keramics_core::open_os_data_stream;
486
487    fn get_file_system() -> Result<ExtFileSystem, ErrorTrace> {
488        let mut file_system: ExtFileSystem = ExtFileSystem::new();
489
490        let path_buf: PathBuf = PathBuf::from("../test_data/ext/ext2.raw");
491        let data_stream: DataStreamReference = open_os_data_stream(&path_buf)?;
492        file_system.read_data_stream(&data_stream)?;
493
494        Ok(file_system)
495    }
496
497    #[test]
498    fn test_get_format_version() -> Result<(), ErrorTrace> {
499        let file_system: ExtFileSystem = get_file_system()?;
500
501        let format_version: u8 = file_system.get_format_version();
502        assert_eq!(format_version, 2);
503
504        Ok(())
505    }
506
507    #[test]
508    fn test_get_feature_flags() -> Result<(), ErrorTrace> {
509        let file_system: ExtFileSystem = get_file_system()?;
510
511        let feature_flags: u32 = file_system.get_compatible_feature_flags();
512        assert_eq!(feature_flags, 0x00000038);
513
514        let feature_flags: u32 = file_system.get_incompatible_feature_flags();
515        assert_eq!(feature_flags, 0x00000002);
516
517        let feature_flags: u32 = file_system.get_read_only_compatible_feature_flags();
518        assert_eq!(feature_flags, 0x00000003);
519
520        Ok(())
521    }
522
523    // TODO: add tests for get_volume_label
524
525    #[test]
526    fn test_get_file_entry_by_identifier() -> Result<(), ErrorTrace> {
527        let file_system: ExtFileSystem = get_file_system()?;
528
529        let file_entry: ExtFileEntry = file_system.get_file_entry_by_identifier(12)?;
530
531        assert_eq!(file_entry.inode_number, 12);
532
533        let name: Option<&ByteString> = file_entry.get_name();
534        assert!(name.is_none());
535
536        Ok(())
537    }
538
539    #[test]
540    fn test_get_file_entry_by_path() -> Result<(), ErrorTrace> {
541        let file_system: ExtFileSystem = get_file_system()?;
542
543        let ext_path: ExtPath = ExtPath::from("/emptyfile");
544        let file_entry: ExtFileEntry = file_system.get_file_entry_by_path(&ext_path)?.unwrap();
545
546        assert_eq!(file_entry.inode_number, 12);
547
548        let ext_path: ExtPath = ExtPath::from("/testdir1/testfile1");
549        let file_entry: ExtFileEntry = file_system.get_file_entry_by_path(&ext_path)?.unwrap();
550
551        assert_eq!(file_entry.inode_number, 14);
552
553        let name: &ByteString = file_entry.get_name().unwrap();
554        assert_eq!(name.to_string(), "testfile1");
555
556        Ok(())
557    }
558
559    #[test]
560    fn test_get_root_directory() -> Result<(), ErrorTrace> {
561        let file_system: ExtFileSystem = get_file_system()?;
562
563        let file_entry: ExtFileEntry = file_system.get_root_directory()?.unwrap();
564
565        assert_eq!(file_entry.inode_number, 2);
566
567        Ok(())
568    }
569
570    #[test]
571    fn test_read_data_stream() -> Result<(), ErrorTrace> {
572        let mut file_system: ExtFileSystem = ExtFileSystem::new();
573
574        let path_buf: PathBuf = PathBuf::from("../test_data/ext/ext2.raw");
575        let data_stream: DataStreamReference = open_os_data_stream(&path_buf)?;
576        file_system.read_data_stream(&data_stream)?;
577
578        assert_eq!(file_system.block_size, 1024);
579        assert_eq!(file_system.inode_size, 128);
580
581        Ok(())
582    }
583
584    #[test]
585    fn test_read_block_groups() -> Result<(), ErrorTrace> {
586        let mut file_system: ExtFileSystem = ExtFileSystem::new();
587
588        let path_buf: PathBuf = PathBuf::from("../test_data/ext/ext2.raw");
589        let data_stream: DataStreamReference = open_os_data_stream(&path_buf)?;
590        file_system.read_block_groups(&data_stream)?;
591
592        assert_eq!(file_system.block_size, 1024);
593        assert_eq!(file_system.inode_size, 128);
594
595        Ok(())
596    }
597}