Skip to main content

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                let filename_str = if is_casefolded {
111                    // We must have valid UTF-8 for casefolding.
112                    String::from_utf8(filename)
113                        .context("Bad UTF-8 in encrypted casefolded filename")?
114                } else {
115                    let filename_len = filename.len();
116                    String::from_utf8(filename)
117                        .unwrap_or_else(|_| format!("BAD_ENCRYPTED_FILENAME_len_{filename_len}"))
118                };
119
120                // If using both encryption and casefold, use hkdf-seeded hash instead.
121                let hash_code = if is_casefolded {
122                    fscrypt::direntry::casefold_encrypt_hash_filename(
123                        filename_str.as_str().into(),
124                        &decryptor.dirhash_key(),
125                    )
126                } else {
127                    fscrypt::direntry::tea_hash_filename(raw_filename.iter().copied())
128                };
129
130                let target = dentry[i].hash_code;
131                ensure!(target == hash_code, "hash_code doesn't match expectation");
132                filename_str
133            } else {
134                // TODO(b/457570701): need better coverage of casefold + encrypted in test image.
135                ProxyFilename::new_with_hash_code(entry.hash_code as u64, &raw_filename).into()
136            }
137        } else {
138            str::from_utf8(&raw_filename).context("Bad UTF8 filename")?.to_string()
139        };
140        let file_type = FileType::n(dentry[i].file_type).ok_or_else(|| anyhow!("Bad file type"))?;
141        out.push(DirEntry {
142            hash_code: entry.hash_code,
143            ino: entry.ino,
144            filename,
145            file_type,
146            raw_filename,
147        });
148        i += (name_len + NAME_LEN - 1) / NAME_LEN;
149    }
150    Ok(out)
151}
152
153impl DentryBlock {
154    pub fn get_entries(
155        &self,
156        ino: u32,
157        is_encrypted: bool,
158        is_casefolded: bool,
159        decryptor: &Option<PerFileDecryptor>,
160    ) -> Result<Vec<DirEntry>, Error> {
161        get_dir_entries(
162            ino,
163            &self.dentry_bitmap,
164            &self.dentry,
165            &self.filenames,
166            is_encrypted,
167            is_casefolded,
168            decryptor,
169        )
170    }
171}
172
173impl crate::inode::Inode {
174    pub fn get_inline_dir_entries(
175        &self,
176        is_encrypted: bool,
177        is_casefolded: bool,
178        decryptor: &Option<PerFileDecryptor>,
179    ) -> Result<Option<Vec<DirEntry>>, Error> {
180        if let Some(inline_dentry) = &self.inline_dentry {
181            Ok(Some(get_dir_entries(
182                self.footer.ino,
183                &inline_dentry.dentry_bitmap,
184                &inline_dentry.dentry,
185                &inline_dentry.filenames,
186                is_encrypted,
187                is_casefolded,
188                decryptor,
189            )?))
190        } else {
191            Ok(None)
192        }
193    }
194}
195
196impl InlineDentry {
197    pub fn try_from_bytes(rest: &[u8]) -> Result<Self, Error> {
198        ensure!(rest.len() % 4 == 0, "Bad alignment in inode inline_dentry");
199        // inline data skips 4 additional bytes.
200        let rest = &rest[4..];
201        // The layout of an inline dentry block is:
202        // +------------------+
203        // | dentry_bitmap    | <-- N bits long, rounded up to next byte.
204        // +------------------+
205        // |    <padding>     |
206        // +------------------+
207        // | N x RawDirEntry  | <-- N * 11 bytes
208        // +------------------+
209        // | N x filenames    | <-- N * 8 bytes
210        // +------------------+
211        // Within the block all elements are byte-aligned.
212        // Note that filenames and RawDirEntry are aligned to the end of the block whilst
213        // dentry_bitmap and the RawDirEntry are aligned to the start.
214        // (This is similar to the layout of DentryBlock.)
215        //
216        // There may be up to 19 bytes of padding between dentry_bitmap and RawDirEntry.
217        // Nb: The following calculation is done in bits to account for the bitmap.
218        let dentry_count =
219            8 * rest.len() / (8 * (std::mem::size_of::<RawDirEntry>() + NAME_LEN) + 1);
220        let (dentry_bitmap, rest): (Ref<_, [u8]>, _) =
221            Ref::from_prefix_with_elems(rest, (dentry_count + 7) / 8).unwrap();
222        let (rest, filenames): (_, Ref<_, [u8]>) =
223            Ref::from_suffix_with_elems(rest, dentry_count * NAME_LEN).unwrap();
224        // Nb: Alignment here is byte-aligned.
225        let (_, dentry): (_, Ref<_, [RawDirEntry]>) =
226            Ref::from_suffix_with_elems(rest, dentry_count).unwrap();
227        Ok(InlineDentry {
228            dentry_bitmap: (*dentry_bitmap).into(),
229            dentry: (*dentry).into(),
230            filenames: (*filenames).into(),
231        })
232    }
233}