ext4_view/
file.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::Ext4;
10use crate::block_index::FsBlockIndex;
11use crate::error::Ext4Error;
12use crate::inode::Inode;
13use crate::iters::file_blocks::FileBlocks;
14use crate::metadata::Metadata;
15use crate::path::Path;
16use crate::resolve::FollowSymlinks;
17use crate::util::usize_from_u32;
18use core::fmt::{self, Debug, Formatter};
19
20#[cfg(feature = "std")]
21use std::io::{self, ErrorKind, Read, Seek, SeekFrom};
22
23/// An open file within an [`Ext4`] filesystem.
24pub struct File {
25    fs: Ext4,
26    inode: Inode,
27    file_blocks: FileBlocks,
28
29    /// Current byte offset within the file.
30    position: u64,
31
32    /// Current block within the file. This is an absolute block index
33    /// within the filesystem.
34    ///
35    /// If `None`, either the next block needs to be fetched from the
36    /// `file_blocks` iterator, or the end of the file has been reached.
37    block_index: Option<FsBlockIndex>,
38}
39
40impl File {
41    /// Open the file at `path`.
42    pub(crate) fn open(fs: &Ext4, path: Path<'_>) -> Result<Self, Ext4Error> {
43        let inode = fs.path_to_inode(path, FollowSymlinks::All)?;
44
45        if inode.metadata.is_dir() {
46            return Err(Ext4Error::IsADirectory);
47        }
48        if !inode.metadata.file_type.is_regular_file() {
49            return Err(Ext4Error::IsASpecialFile);
50        }
51
52        Self::open_inode(fs, inode)
53    }
54
55    /// Open `inode`. Note that unlike `File::open`, this allows any
56    /// type of `inode` to be opened, including directories and
57    /// symlinks. This is used by `Ext4::read_inode_file`.
58    pub(crate) fn open_inode(
59        fs: &Ext4,
60        inode: Inode,
61    ) -> Result<Self, Ext4Error> {
62        Ok(Self {
63            fs: fs.clone(),
64            position: 0,
65            file_blocks: FileBlocks::new(fs.clone(), &inode)?,
66            inode,
67            block_index: None,
68        })
69    }
70
71    /// Get the file metadata.
72    #[must_use]
73    pub fn metadata(&self) -> &Metadata {
74        &self.inode.metadata
75    }
76
77    /// Read bytes from the file into `buf`, returning how many bytes
78    /// were read. The number may be smaller than the length of the
79    /// input buffer.
80    ///
81    /// This advances the position of the file by the number of bytes
82    /// read, so calling `read_bytes` repeatedly can be used to read the
83    /// entire file.
84    ///
85    /// Returns `Ok(0)` if the end of the file has been reached.
86    pub fn read_bytes(
87        &mut self,
88        mut buf: &mut [u8],
89    ) -> Result<usize, Ext4Error> {
90        // Nothing to do if output buffer is empty.
91        if buf.is_empty() {
92            return Ok(0);
93        }
94
95        // Nothing to do if already at the end of the file.
96        if self.position >= self.inode.metadata.size_in_bytes {
97            return Ok(0);
98        }
99
100        // Get the number of bytes remaining in the file, starting from
101        // the current `position`.
102        //
103        // OK to unwrap: just checked that `position` is less than the
104        // file size.
105        let bytes_remaining = self
106            .inode
107            .metadata
108            .size_in_bytes
109            .checked_sub(self.position)
110            .unwrap();
111
112        // If the the number of bytes remaining is less than the output
113        // buffer length, shrink the buffer.
114        //
115        // If the conversion to `usize` fails, the output buffer is
116        // definitely not larger than the remaining bytes to read.
117        if let Ok(bytes_remaining) = usize::try_from(bytes_remaining) {
118            if buf.len() > bytes_remaining {
119                buf = &mut buf[..bytes_remaining];
120            }
121        }
122
123        let block_size = self.fs.0.superblock.block_size;
124
125        // Get the block to read from.
126        let block_index = if let Some(block_index) = self.block_index {
127            block_index
128        } else {
129            // OK to unwrap: already checked that the position is not at
130            // the end of the file, so there must be at least one more
131            // block to read.
132            let block_index = self.file_blocks.next().unwrap()?;
133
134            self.block_index = Some(block_index);
135
136            block_index
137        };
138
139        // Byte offset within the current block.
140        //
141        // OK to unwrap: block size fits in a `u32`, so an offset within
142        // the block will as well.
143        let offset_within_block: u32 =
144            u32::try_from(self.position % block_size.to_nz_u64()).unwrap();
145
146        // OK to unwrap: `offset_within_block` is always less than or
147        // equal to the block length.
148        //
149        // Note that if this block is at the end of the file, the block
150        // may extend past the actual number of bytes in the file. This
151        // does not matter because the output buffer's length was
152        // already capped earlier against the number of bytes remaining
153        // in the file.
154        let bytes_remaining_in_block: u32 = block_size
155            .to_u32()
156            .checked_sub(offset_within_block)
157            .unwrap();
158
159        // If the output buffer is larger than the number of bytes
160        // remaining in the block, shink the buffer.
161        if buf.len() > usize_from_u32(bytes_remaining_in_block) {
162            buf = &mut buf[..usize_from_u32(bytes_remaining_in_block)];
163        }
164
165        // OK to unwrap: the buffer length has been capped so that it
166        // cannot be larger than the block size, and the block size fits
167        // in a `u32`.
168        let buf_len_u32: u32 = buf.len().try_into().unwrap();
169
170        // Read the block data, or zeros if in a hole.
171        if block_index == 0 {
172            buf.fill(0);
173        } else {
174            self.fs
175                .read_from_block(block_index, offset_within_block, buf)?;
176        }
177
178        // OK to unwrap: reads don't extend past a block, so this is at
179        // most `block_size`, which always fits in a `u32`.
180        let new_offset_within_block: u32 =
181            offset_within_block.checked_add(buf_len_u32).unwrap();
182
183        // If the end of this block has been reached, clear
184        // `self.block_index` so that the next call fetches a new block
185        // from the iterator.
186        if new_offset_within_block >= block_size {
187            self.block_index = None;
188        }
189
190        // OK to unwrap: the buffer length is capped such that this
191        // calculation is at most the length of the file, which fits in
192        // a `u64`.
193        self.position =
194            self.position.checked_add(u64::from(buf_len_u32)).unwrap();
195
196        Ok(buf.len())
197    }
198
199    /// Current position within the file.
200    #[must_use]
201    pub fn position(&self) -> u64 {
202        self.position
203    }
204
205    /// Seek from the start of the file to `position`.
206    ///
207    /// Seeking past the end of the file is allowed.
208    pub fn seek_to(&mut self, position: u64) -> Result<(), Ext4Error> {
209        // Reset iteration.
210        self.file_blocks = FileBlocks::new(self.fs.clone(), &self.inode)?;
211        self.block_index = None;
212
213        // Advance the block iterator by the number of whole blocks in
214        // `position`.
215        let num_blocks = position / self.fs.0.superblock.block_size.to_nz_u64();
216        for _ in 0..num_blocks {
217            self.file_blocks.next();
218        }
219
220        self.position = position;
221
222        Ok(())
223    }
224}
225
226impl Debug for File {
227    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
228        f.debug_struct("File")
229            // Just show the index from `self.inode`, the full `Inode`
230            // output is verbose.
231            .field("inode", &self.inode.index)
232            .field("position", &self.position)
233            // Don't show all fields, as that would make the output less
234            // readable.
235            .finish_non_exhaustive()
236    }
237}
238
239#[cfg(feature = "std")]
240impl Read for File {
241    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
242        Ok(self.read_bytes(buf)?)
243    }
244}
245
246#[cfg(feature = "std")]
247impl Seek for File {
248    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
249        let pos = match pos {
250            SeekFrom::Start(pos) => pos,
251            SeekFrom::End(offset) => {
252                // file_size + offset:
253                i64::try_from(self.inode.metadata.size_in_bytes)
254                    .ok()
255                    .and_then(|pos| pos.checked_add(offset))
256                    .and_then(|pos| pos.try_into().ok())
257                    .ok_or(ErrorKind::InvalidInput)?
258            }
259            SeekFrom::Current(offset) => {
260                // current_pos + offset:
261                i64::try_from(self.position)
262                    .ok()
263                    .and_then(|pos| pos.checked_add(offset))
264                    .and_then(|pos| pos.try_into().ok())
265                    .ok_or(ErrorKind::InvalidInput)?
266            }
267        };
268
269        self.seek_to(pos)?;
270
271        Ok(self.position)
272    }
273}