Skip to main content

ext4_lib/
processor.rs

1// Copyright 2026 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.
4
5use crate::parser::Parser;
6use crate::readers::ReaderWriter;
7use crate::structs::{FIRST_BG_PADDING, InvalidAddressErrorType, ParsingError};
8use fuchsia_sync::Mutex;
9use futures::future::FutureExt;
10use std::collections::HashMap;
11use std::sync::Arc;
12
13#[derive(Default)]
14pub struct Ext4FileMetrics {
15    num_read_requests: u64,
16    num_open_requests: u64,
17    num_truncate_requests: u64,
18    num_write_requests: u64,
19    num_writes_past_eof_attempts: u64,
20    num_successful_overwrites: u64,
21    num_blocks_overwritten: u64,
22}
23
24/// A processor that wraps an ext4 parser and adds write functionality if not in read-only mode.
25/// Note that syncing of metadata to the persistent storage is *NOT* supported.
26pub struct Ext4Processor {
27    fs: Parser,
28    reader_writer: Arc<dyn ReaderWriter>,
29    read_only: bool,
30    file_metrics: Arc<Mutex<Ext4FileMetrics>>,
31    file_size: Mutex<HashMap<u32, u64>>,
32}
33
34impl std::ops::Deref for Ext4Processor {
35    type Target = Parser;
36
37    fn deref(&self) -> &Self::Target {
38        &self.fs
39    }
40}
41
42impl Ext4Processor {
43    pub fn new(reader_writer: Arc<dyn ReaderWriter>, read_only: bool) -> Self {
44        Self {
45            fs: Parser::new(Box::new(reader_writer.clone())),
46            reader_writer,
47            read_only,
48            file_metrics: Arc::new(Mutex::new(Ext4FileMetrics::default())),
49            file_size: Mutex::new(HashMap::new()),
50        }
51    }
52
53    pub fn record_read_metrics(&self) {
54        let mut metrics = self.file_metrics.lock();
55        metrics.num_read_requests += 1;
56    }
57
58    pub fn record_open_metrics(&self) {
59        let mut metrics = self.file_metrics.lock();
60        metrics.num_open_requests += 1;
61    }
62
63    pub fn record_statistics(&self, stats_node: &fuchsia_inspect::Node) {
64        let metrics = self.file_metrics.clone();
65        stats_node.record_lazy_child("file_metrics", move || {
66            let metrics = metrics.clone();
67            async move {
68                let inspector = fuchsia_inspect::Inspector::default();
69                let root = inspector.root();
70                let metrics = metrics.lock();
71                root.record_uint("num_read_requests", metrics.num_read_requests);
72                root.record_uint("num_open_requests", metrics.num_open_requests);
73                root.record_uint("num_truncate_requests", metrics.num_truncate_requests);
74                root.record_uint("num_write_requests", metrics.num_write_requests);
75                root.record_uint(
76                    "num_writes_past_eof_attempts",
77                    metrics.num_writes_past_eof_attempts,
78                );
79                root.record_uint("num_successful_overwrites", metrics.num_successful_overwrites);
80                root.record_uint("num_blocks_overwritten", metrics.num_blocks_overwritten);
81                Ok(inspector)
82            }
83            .boxed()
84        });
85    }
86
87    pub fn read_only(&self) -> bool {
88        self.read_only
89    }
90
91    /// Writes contiguous raw data starting at a given block number.
92    fn write_blocks(&self, block_number: u64, data: &[u8]) -> Result<(), ParsingError> {
93        if self.read_only {
94            return Err(ParsingError::Incompatible("Cannot write to read-only Ext4".to_string()));
95        }
96        if block_number == 0 {
97            return Err(ParsingError::InvalidAddress(
98                InvalidAddressErrorType::Lower,
99                0,
100                FIRST_BG_PADDING,
101            ));
102        }
103
104        let block_size = self.block_size()?;
105        if data.len() as u64 % block_size != 0 {
106            return Err(ParsingError::Incompatible(format!(
107                "Data length {} is not a multiple of block size {}",
108                data.len(),
109                block_size
110            )));
111        }
112
113        let address = block_number
114            .checked_mul(block_size)
115            .ok_or(ParsingError::BlockNumberOutOfBounds(block_number))?;
116
117        self.reader_writer.write(address, data)?;
118
119        Ok(())
120    }
121
122    pub fn file_size(&self, inode_num: u32) -> Result<u64, ParsingError> {
123        let file_size = self.file_size.lock();
124        if let Some(&size) = file_size.get(&inode_num) {
125            Ok(size)
126        } else {
127            Ok(self.inode(inode_num)?.size())
128        }
129    }
130
131    pub fn truncate_to_zero(&self, inode_num: u32) -> Result<(), ParsingError> {
132        if self.read_only {
133            return Err(ParsingError::Incompatible("Cannot write to read-only Ext4".to_string()));
134        }
135        let mut file_metrics = self.file_metrics.lock();
136        file_metrics.num_truncate_requests += 1;
137
138        self.file_size.lock().insert(inode_num, 0);
139        Ok(())
140    }
141
142    /// Overwrites existing contents of a file with new data. This does not require any allocations.
143    /// Note that this does not update the journal with timestamps.
144    pub fn overwrite_file_contents(
145        &self,
146        inode_num: u32,
147        data: impl AsRef<[u8]>,
148        offset: u64,
149    ) -> Result<(), ParsingError> {
150        if self.read_only {
151            return Err(ParsingError::Incompatible("Cannot write to read-only Ext4".to_string()));
152        }
153        let mut file_metrics = self.file_metrics.lock();
154        file_metrics.num_write_requests += 1;
155
156        let inode = self.inode(inode_num)?;
157        let persisted_size = inode.size();
158        // If the file has been truncated to zero, we must write to the same size of the persisted
159        // size.
160        if let Some(_) = self.file_size.lock().get(&inode_num) {
161            if offset + data.as_ref().len() as u64 != persisted_size {
162                file_metrics.num_writes_past_eof_attempts += 1;
163                return Err(ParsingError::NotSupported(
164                    "writing past EOF / partial writes".to_string(),
165                ));
166            }
167        }
168        if offset + data.as_ref().len() as u64 > persisted_size {
169            file_metrics.num_writes_past_eof_attempts += 1;
170            return Err(ParsingError::NotSupported(
171                "writing past EOF / partial writes".to_string(),
172            ));
173        }
174        if data.as_ref().len() == 0 {
175            file_metrics.num_successful_overwrites += 1;
176            return Ok(());
177        }
178
179        let root_extent_tree_node = inode.extent_tree_node()?;
180        let request = offset..offset + data.as_ref().len() as u64;
181        let block_size = self.block_size()?;
182
183        self.iterate_extents_in_tree(&root_extent_tree_node, &mut |extent| {
184            let range = (extent.e_blk.get() as u64 * block_size)
185                ..((extent.e_blk.get() as u64 + extent.e_len.get() as u64) * block_size);
186            let overlap =
187                std::cmp::max(range.start, request.start)..std::cmp::min(range.end, request.end);
188            if overlap.start >= overlap.end {
189                // No overlap.
190                return Ok(());
191            }
192
193            let mut physical_block_cursor =
194                extent.target_block_num() + ((overlap.start - range.start) / block_size);
195            let mut current_offset = overlap.start;
196            while current_offset < overlap.end {
197                let write_buf_cursor = (current_offset - request.start) as usize;
198                let block_off = current_offset % block_size;
199                let remaining_in_overlap = overlap.end - current_offset;
200
201                if block_off == 0 && remaining_in_overlap >= block_size {
202                    // Contiguous full blocks write
203                    let full_blocks = remaining_in_overlap / block_size;
204                    let write_len = full_blocks * block_size;
205                    self.write_blocks(
206                        physical_block_cursor,
207                        &data.as_ref()[write_buf_cursor..write_buf_cursor + write_len as usize],
208                    )?;
209                    file_metrics.num_blocks_overwritten += full_blocks;
210
211                    physical_block_cursor += full_blocks;
212                    current_offset += write_len;
213                } else {
214                    // Write partial block by first reading the existing block and overwriting the
215                    // relevant part.
216                    let remaining_in_block = block_size - block_off;
217                    let write_len = std::cmp::min(remaining_in_block, remaining_in_overlap);
218                    let mut block_data = self.block(physical_block_cursor)?.into_vec();
219                    block_data[block_off as usize..block_off as usize + write_len as usize]
220                        .copy_from_slice(
221                            &data.as_ref()[write_buf_cursor..write_buf_cursor + write_len as usize],
222                        );
223                    self.write_blocks(physical_block_cursor, &block_data)?;
224                    file_metrics.num_blocks_overwritten += 1;
225
226                    physical_block_cursor += 1;
227                    current_offset += write_len;
228                }
229            }
230            Ok(())
231        })?;
232
233        // Update size: size may be be 0 if `truncate_to_zero` was called before this. Removing it
234        // means the next call to `file_size` will return the persisted size from the inode.
235        self.file_size.lock().remove(&inode_num);
236
237        file_metrics.num_successful_overwrites += 1;
238        Ok(())
239    }
240
241    pub fn sync(&self) -> Result<(), ParsingError> {
242        self.reader_writer.sync()?;
243        Ok(())
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::readers::{BlockDeviceReader, VecReader};
251    use crate::structs::{FIRST_BG_PADDING, InvalidAddressErrorType, ParsingError};
252    use std::fs;
253    use std::path::Path;
254    use vmo_backed_block_server::{InitialContents, VmoBackedServerOptions};
255    use zx::Vmo;
256    use {fidl_fuchsia_storage_block as fblock, fuchsia_async as fasync};
257
258    #[fuchsia::test]
259    async fn test_processor_read_only_blocks_write() {
260        let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
261        let read_only_processor = Ext4Processor::new(Arc::new(VecReader::new(data)), true);
262
263        let error = read_only_processor
264            .write_blocks(1, &[0u8; 1024])
265            .expect_err("passed write_blocks unexpectedly");
266        match error {
267            ParsingError::Incompatible(_) => {}
268            _ => panic!("Expected read-only error"),
269        }
270
271        // Test overwrite_file_contents
272        let error = read_only_processor
273            .overwrite_file_contents(2, &[0u8; 10], 0)
274            .expect_err("passed overwrite_file_contents unexpectedly");
275        match error {
276            ParsingError::Incompatible(_) => {}
277            _ => panic!("Expected read-only error"),
278        }
279    }
280
281    #[fuchsia::test]
282    async fn test_processor_write_block_invalid_address() {
283        let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
284        let processor = Ext4Processor::new(Arc::new(VecReader::new(data)), false);
285
286        let error =
287            processor.write_blocks(0, &[0u8; 1024]).expect_err("passed write_blocks unexpectedly");
288        match error {
289            ParsingError::InvalidAddress(InvalidAddressErrorType::Lower, 0, FIRST_BG_PADDING) => {}
290            _ => panic!("Expected invalid address error, got {:?}", error),
291        }
292    }
293
294    #[fuchsia::test]
295    async fn test_processor_write_block_out_of_bounds() {
296        let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
297        let processor = Ext4Processor::new(Arc::new(VecReader::new(data)), false);
298
299        let error = processor
300            .write_blocks(u64::MAX, &[0u8; 1024])
301            .expect_err("passed write_blocks unexpectedly");
302        match error {
303            ParsingError::BlockNumberOutOfBounds(u64::MAX) => {}
304            _ => panic!("Expected out of bounds error, got {:?}", error),
305        }
306    }
307
308    #[fuchsia::test]
309    async fn test_processor_writeable_overwrite_extents() {
310        let data = fs::read("/pkg/data/1file.img").expect("Unable to read file");
311        let vmo = Vmo::create(data.len() as u64).expect("failed to create VMO");
312        vmo.write(data.as_slice(), 0).expect("failed to write to VMO");
313        let server = Arc::new(
314            VmoBackedServerOptions {
315                block_size: 512,
316                initial_contents: InitialContents::FromVmo(vmo),
317                ..Default::default()
318            }
319            .build()
320            .expect("build from VmoBackedServerOptions failed"),
321        );
322
323        let server_clone = server.clone();
324        let (block_client_end1, block_server_end1) =
325            fidl::endpoints::create_endpoints::<fblock::BlockMarker>();
326        std::thread::spawn(move || {
327            let mut executor = fasync::TestExecutor::new();
328            let _task =
329                executor.run_singlethreaded(server_clone.serve(block_server_end1.into_stream()));
330        });
331        let inspector = fuchsia_inspect::Inspector::default();
332        let rw_processor = Arc::new(Ext4Processor::new(
333            Arc::new(
334                BlockDeviceReader::from_client_end(block_client_end1)
335                    .expect("failed to create block device reader"),
336            ),
337            false,
338        ));
339        rw_processor.record_statistics(inspector.root());
340
341        let file_ino = rw_processor
342            .entry_at_path(Path::new("file1"))
343            .expect("failed entry at path")
344            .e2d_ino
345            .get();
346
347        let mut expected = rw_processor.read_data(file_ino).expect("failed to read data");
348        assert_eq!(
349            str::from_utf8(expected.as_slice()).expect("failed to read data"),
350            "file1 contents.\n"
351        );
352        let original_size = rw_processor.inode(file_ino).expect("failed to read inode").size();
353        assert_eq!(original_size, expected.len() as u64);
354
355        rw_processor
356            .overwrite_file_contents(file_ino, &[1u8; 1], 1)
357            .expect("failed to overwrite extents");
358        expected[1] = 1;
359
360        let new_data = rw_processor.read_data(file_ino).expect("failed to read data");
361        assert_eq!(new_data, expected);
362        diagnostics_assertions::assert_data_tree!(inspector, root: {
363            file_metrics: {
364                num_open_requests: 0u64,
365                num_read_requests: 0u64,
366                num_truncate_requests: 0u64,
367                num_write_requests: 1u64,
368                num_writes_past_eof_attempts: 0u64,
369                num_successful_overwrites: 1u64,
370                num_blocks_overwritten: 1u64,
371            }
372        });
373
374        // Test writing to the allocated extent, extending past the original file size (still within
375        // the allocated block).
376        let error = rw_processor
377            .overwrite_file_contents(file_ino, &[1u8; 2], expected.len() as u64 + 2)
378            .expect_err("overwrite past EOF should fail");
379        match error {
380            ParsingError::NotSupported(_) => {}
381            _ => panic!("Expected NotSupported error, got {:?}", error),
382        }
383
384        // Verify that the file size has not updated.
385        let new_size = rw_processor.inode(file_ino).expect("failed to read inode").size();
386        assert_eq!(new_size, original_size);
387        diagnostics_assertions::assert_data_tree!(inspector, root: {
388            file_metrics: {
389                num_open_requests: 0u64,
390                num_read_requests: 0u64,
391                num_truncate_requests: 0u64,
392                num_write_requests: 2u64,
393                num_writes_past_eof_attempts: 1u64,
394                num_successful_overwrites: 1u64,
395                num_blocks_overwritten: 1u64,
396            }
397        });
398    }
399}