Skip to main content

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