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