Skip to main content

quantydb_sqlite/
source.rs

1//! Where the bytes come from.
2//!
3//! This is deliberately not `quantydb_core::Storage`. That trait can write,
4//! and the file we are reading belongs to somebody else: it is their
5//! database, produced by a different engine, and we are only ever a guest
6//! in it. A trait without a write method makes damaging the source
7//! impossible to express, rather than merely something we remember not to
8//! do. It also lets the file backend open read only, so importing from a
9//! read only mount or a file we lack write permission on works.
10//!
11//! The slice backend is what the fuzz harness drives, so the two paths that
12//! matter are covered by the same code above this line.
13
14use std::fs::File;
15use std::path::Path;
16
17use crate::error::{Result, SqliteError};
18
19pub trait Source {
20    /// Read exactly `buf.len()` bytes at `offset`. Reading past the end is
21    /// an error; short reads are never returned.
22    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()>;
23
24    /// Size in bytes.
25    fn len(&self) -> Result<u64>;
26
27    fn is_empty(&self) -> Result<bool> {
28        Ok(self.len()? == 0)
29    }
30
31    /// Whether this source has already accounted for a write-ahead log.
32    ///
33    /// A wal mode database is only complete in its main file once the log
34    /// has been checkpointed into it, and bytes alone cannot say whether
35    /// that happened. A source that knows, because it looked next to the
36    /// file or because it reads the log itself, says so here; everything
37    /// else stays conservative and a wal mode database read through it is
38    /// refused rather than silently truncated to an older version.
39    fn accounts_for_wal(&self) -> bool {
40        false
41    }
42}
43
44/// A file opened read only.
45pub struct FileSource {
46    file: File,
47    /// True when there is no log to account for: either no `-wal` file
48    /// sits next to this one, or the one that does is empty, which is what
49    /// sqlite leaves behind after a checkpoint.
50    no_log_beside_it: bool,
51}
52
53impl FileSource {
54    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
55        let path = path.as_ref();
56        let file = File::open(path)?;
57        // sqlite names the log after the database, suffix and all
58        let mut log = path.as_os_str().to_os_string();
59        log.push("-wal");
60        let no_log_beside_it = match std::fs::metadata(Path::new(&log)) {
61            Ok(meta) => meta.len() == 0,
62            Err(_) => true,
63        };
64        Ok(FileSource {
65            file,
66            no_log_beside_it,
67        })
68    }
69}
70
71#[cfg(unix)]
72impl Source for FileSource {
73    fn accounts_for_wal(&self) -> bool {
74        self.no_log_beside_it
75    }
76
77    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
78        use std::os::unix::fs::FileExt;
79        self.file.read_exact_at(buf, offset)?;
80        Ok(())
81    }
82
83    fn len(&self) -> Result<u64> {
84        Ok(self.file.metadata()?.len())
85    }
86}
87
88#[cfg(windows)]
89impl Source for FileSource {
90    fn accounts_for_wal(&self) -> bool {
91        self.no_log_beside_it
92    }
93
94    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
95        use std::os::windows::fs::FileExt;
96        let mut done = 0;
97        while done < buf.len() {
98            let n = self
99                .file
100                .seek_read(&mut buf[done..], offset + done as u64)?;
101            if n == 0 {
102                return Err(SqliteError::Io(std::io::Error::from(
103                    std::io::ErrorKind::UnexpectedEof,
104                )));
105            }
106            done += n;
107        }
108        Ok(())
109    }
110
111    fn len(&self) -> Result<u64> {
112        Ok(self.file.metadata()?.len())
113    }
114}
115
116/// Bytes already in memory. Used by the tests and by the fuzz harness.
117pub struct SliceSource<'a> {
118    bytes: &'a [u8],
119}
120
121impl<'a> SliceSource<'a> {
122    pub fn new(bytes: &'a [u8]) -> Self {
123        SliceSource { bytes }
124    }
125}
126
127impl Source for SliceSource<'_> {
128    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
129        let start = usize::try_from(offset).map_err(|_| {
130            SqliteError::Io(std::io::Error::from(std::io::ErrorKind::UnexpectedEof))
131        })?;
132        let end = start.checked_add(buf.len()).ok_or_else(|| {
133            SqliteError::Io(std::io::Error::from(std::io::ErrorKind::UnexpectedEof))
134        })?;
135        if end > self.bytes.len() {
136            return Err(SqliteError::Io(std::io::Error::from(
137                std::io::ErrorKind::UnexpectedEof,
138            )));
139        }
140        buf.copy_from_slice(&self.bytes[start..end]);
141        Ok(())
142    }
143
144    fn len(&self) -> Result<u64> {
145        Ok(self.bytes.len() as u64)
146    }
147}