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