f2fs_reader/
dir.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4use crate::crypto::PerFileDecryptor;
5use crate::superblock::BLOCK_SIZE;
6use anyhow::{Context, Error, anyhow, ensure};
7use enumn::N;
8use fscrypt::proxy_filename::ProxyFilename;
9use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned};
10
11#[derive(Copy, Clone, Debug, Eq, PartialEq, N)]
12#[repr(u8)]
13pub enum FileType {
14    Unknown = 0,
15    RegularFile = 1,
16    Directory = 2,
17    CharDevice = 3,
18    BlockDevice = 4,
19    Fifo = 5,
20    Socket = 6,
21    Symlink = 7,
22}
23
24#[repr(C, packed)]
25#[derive(Copy, Clone, Debug, FromBytes, IntoBytes, Immutable, KnownLayout, Unaligned)]
26pub struct RawDirEntry {
27    pub hash_code: u32,
28    pub ino: u32,
29    pub name_len: u16,
30    pub file_type: u8,
31}
32
33#[derive(Clone, Debug)]
34pub struct InlineDentry {
35    pub dentry_bitmap: Box<[u8]>,
36    pub dentry: Box<[RawDirEntry]>,
37    pub filenames: Box<[u8]>,
38}
39
40pub const NAME_LEN: usize = 8;
41pub const NUM_DENTRY_IN_BLOCK: usize = 214;
42/// One bit per entry rounded up to the next byte.
43pub const SIZE_OF_DENTRY_BITMAP: usize = (NUM_DENTRY_IN_BLOCK + 7) / 8;
44/// Reserve space ensures we fill the block.
45pub const SIZE_OF_DENTRY_RESERVED: usize = BLOCK_SIZE
46    - ((std::mem::size_of::<RawDirEntry>() + NAME_LEN) * NUM_DENTRY_IN_BLOCK
47        + SIZE_OF_DENTRY_BITMAP);
48
49#[repr(C, packed)]
50#[derive(Copy, Clone, Debug, FromBytes, Immutable, KnownLayout, IntoBytes, Unaligned)]
51pub struct DentryBlock {
52    dentry_bitmap: [u8; SIZE_OF_DENTRY_BITMAP],
53    _reserved: [u8; SIZE_OF_DENTRY_RESERVED],
54    dentry: [RawDirEntry; NUM_DENTRY_IN_BLOCK],
55    filenames: [u8; NUM_DENTRY_IN_BLOCK * NAME_LEN],
56}
57
58#[derive(Debug)]
59pub struct DirEntry {
60    pub hash_code: u32,
61    pub ino: u32,
62    pub filename: String,
63    pub file_type: FileType,
64    pub raw_filename: Vec<u8>,
65}
66
67// Helper function for reading directory entries.
68// Caller is required to ensure that these byte arrays are appropriately sized to avoid panics.
69fn get_dir_entries(
70    ino: u32,
71    dentry_bitmap: &[u8],
72    dentry: &[RawDirEntry],
73    filenames: &[u8],
74    is_encrypted: bool,
75    is_casefolded: bool,
76    decryptor: &Option<PerFileDecryptor>,
77) -> Result<Vec<DirEntry>, Error> {
78    debug_assert!(dentry_bitmap.len() * 8 >= dentry.len(), "bitmap too small");
79    debug_assert_eq!(dentry.len() * 8, filenames.len(), "dentry len different to filenames len");
80    let mut out = Vec::new();
81    let mut i = 0;
82    while i < dentry.len() {
83        let entry = dentry[i];
84        // The dentry bitmap marks all entries that should be read.
85        if (dentry_bitmap[i / 8] >> (i % 8)) & 0x1 == 0 || dentry[i].ino == 0 {
86            i += 1;
87            continue;
88        }
89        let name_len = dentry[i].name_len as usize;
90        if name_len == 0 {
91            i += 1;
92            continue;
93        }
94        // Filename slots are 8 bytes long but F2fs allows long filenames to span multiple slots.
95        ensure!(i * NAME_LEN + name_len <= filenames.len(), "Filename doesn't fit in buffer");
96        let raw_filename = filenames[i * NAME_LEN..i * NAME_LEN + name_len].to_vec();
97        // Ignore dot files.
98        if raw_filename == b"." || raw_filename == b".." {
99            i += 1;
100            continue;
101        }
102        // TODO(https://fxbug.dev/404680707): Do we need to consider handling devices with badly formed filenames?
103        let filename = if is_encrypted {
104            if let Some(decryptor) = decryptor {
105                let mut filename = raw_filename.clone();
106                decryptor.decrypt_filename_data(ino, &mut filename);
107                while filename.last() == Some(&0) {
108                    filename.pop();
109                }
110                // If using both encryption and casefold, use hkdf-seeded hash instead.
111                let hash_code = if is_casefolded {
112                    fscrypt::direntry::casefold_encrypt_hash_filename(
113                        filename.as_slice(),
114                        &decryptor.dirhash_key(),
115                    )
116                } else {
117                    fscrypt::direntry::tea_hash_filename(raw_filename.as_slice())
118                };
119
120                let target = dentry[i].hash_code;
121                ensure!(target == hash_code, "hash_code doesn't match expectation");
122                let filename_len = filename.len();
123                String::from_utf8(filename)
124                    .unwrap_or_else(|_| format!("BAD_ENCRYPTED_FILENAME_len_{filename_len}"))
125            } else {
126                // TODO(b/457570701): need better coverage of casefold + encrypted in test image.
127                ProxyFilename::new_with_hash_code(entry.hash_code as u64, &raw_filename).into()
128            }
129        } else {
130            str::from_utf8(&raw_filename).context("Bad UTF8 filename")?.to_string()
131        };
132        let file_type = FileType::n(dentry[i].file_type).ok_or_else(|| anyhow!("Bad file type"))?;
133        out.push(DirEntry {
134            hash_code: entry.hash_code,
135            ino: entry.ino,
136            filename,
137            file_type,
138            raw_filename,
139        });
140        i += (name_len + NAME_LEN - 1) / NAME_LEN;
141    }
142    Ok(out)
143}
144
145impl DentryBlock {
146    pub fn get_entries(
147        &self,
148        ino: u32,
149        is_encrypted: bool,
150        is_casefolded: bool,
151        decryptor: &Option<PerFileDecryptor>,
152    ) -> Result<Vec<DirEntry>, Error> {
153        get_dir_entries(
154            ino,
155            &self.dentry_bitmap,
156            &self.dentry,
157            &self.filenames,
158            is_encrypted,
159            is_casefolded,
160            decryptor,
161        )
162    }
163}
164
165impl crate::inode::Inode {
166    pub fn get_inline_dir_entries(
167        &self,
168        is_encrypted: bool,
169        is_casefolded: bool,
170        decryptor: &Option<PerFileDecryptor>,
171    ) -> Result<Option<Vec<DirEntry>>, Error> {
172        if let Some(inline_dentry) = &self.inline_dentry {
173            Ok(Some(get_dir_entries(
174                self.footer.ino,
175                &inline_dentry.dentry_bitmap,
176                &inline_dentry.dentry,
177                &inline_dentry.filenames,
178                is_encrypted,
179                is_casefolded,
180                decryptor,
181            )?))
182        } else {
183            Ok(None)
184        }
185    }
186}
187
188impl InlineDentry {
189    pub fn try_from_bytes(rest: &[u8]) -> Result<Self, Error> {
190        ensure!(rest.len() % 4 == 0, "Bad alignment in inode inline_dentry");
191        // inline data skips 4 additional bytes.
192        let rest = &rest[4..];
193        // The layout of an inline dentry block is:
194        // +------------------+
195        // | dentry_bitmap    | <-- N bits long, rounded up to next byte.
196        // +------------------+
197        // |    <padding>     |
198        // +------------------+
199        // | N x RawDirEntry  | <-- N * 11 bytes
200        // +------------------+
201        // | N x filenames    | <-- N * 8 bytes
202        // +------------------+
203        // Within the block all elements are byte-aligned.
204        // Note that filenames and RawDirEntry are aligned to the end of the block whilst
205        // dentry_bitmap and the RawDirEntry are aligned to the start.
206        // (This is similar to the layout of DentryBlock.)
207        //
208        // There may be up to 19 bytes of padding between dentry_bitmap and RawDirEntry.
209        // Nb: The following calculation is done in bits to account for the bitmap.
210        let dentry_count =
211            8 * rest.len() / (8 * (std::mem::size_of::<RawDirEntry>() + NAME_LEN) + 1);
212        let (dentry_bitmap, rest): (Ref<_, [u8]>, _) =
213            Ref::from_prefix_with_elems(rest, (dentry_count + 7) / 8).unwrap();
214        let (rest, filenames): (_, Ref<_, [u8]>) =
215            Ref::from_suffix_with_elems(rest, dentry_count * NAME_LEN).unwrap();
216        // Nb: Alignment here is byte-aligned.
217        let (_, dentry): (_, Ref<_, [RawDirEntry]>) =
218            Ref::from_suffix_with_elems(rest, dentry_count).unwrap();
219        Ok(InlineDentry {
220            dentry_bitmap: (*dentry_bitmap).into(),
221            dentry: (*dentry).into(),
222            filenames: (*filenames).into(),
223        })
224    }
225}