| 1 | use super::*; |
| 2 | use onestore::FileDataReference; |
| 3 | use onestore::{ |
| 4 | RevisionIndex, Store, |
| 5 | document::{Document, Kind}, |
| 6 | }; |
| 7 | use rusqlite::OptionalExtension; |
| 8 | use sha2::{Digest, Sha256}; |
| 9 | |
| 10 | impl Replica { |
| 11 | /// Reads a previously downloaded external payload without network access. |
| 12 | /// Absence is distinct from an empty payload; every returned buffer passes its stored checksum. |
| 13 | pub fn cached_asset(&self, filename: &str, limit: usize) -> Result<Option<Vec<u8>>> { |
| 14 | cached(&*self.lock()?, &key(filename)?, limit) |
| 15 | } |
| 16 | |
| 17 | /// Fetches a declared external payload and durably retains it without changing the edit queue. |
| 18 | /// Different bytes for an already cached identity return `AssetChanged`, preserving the cache. |
| 19 | /// Network I/O does not hold the cache mutex; a stale reference fails before local publication. |
| 20 | pub fn fetch_asset( |
| 21 | &self, |
| 22 | source: &mut impl crate::discover::Source, |
| 23 | section: &str, |
| 24 | filename: &str, |
| 25 | limit: usize, |
| 26 | ) -> Result<Vec<u8>> { |
| 27 | let key = key(filename)?; |
| 28 | if !self.references(&key)? { |
| 29 | return Err(io::Error::new( |
| 30 | io::ErrorKind::InvalidInput, |
| 31 | "The retained document images do not reference this external payload", |
| 32 | ) |
| 33 | .into()); |
| 34 | } |
| 35 | let bytes = crate::discover::read_external_asset(source, section, filename, limit)?; |
| 36 | if !self.references(&key)? { |
| 37 | return Err(io::Error::new( |
| 38 | io::ErrorKind::ResourceBusy, |
| 39 | "The external payload reference changed during download", |
| 40 | ) |
| 41 | .into()); |
| 42 | } |
| 43 | let mut connection = self.lock()?; |
| 44 | let transaction = connection.transaction_with_behavior(TransactionBehavior::Immediate)?; |
| 45 | let previous = match cached(&transaction, &key, bytes.len()) { |
| 46 | Err(Error::Io(error)) if error.kind() == io::ErrorKind::FileTooLarge => { |
| 47 | return Err(Error::AssetChanged); |
| 48 | } |
| 49 | other => other?, |
| 50 | }; |
| 51 | if let Some(previous) = previous { |
| 52 | if previous != bytes { |
| 53 | return Err(Error::AssetChanged); |
| 54 | } |
| 55 | } else { |
| 56 | transaction.execute( |
| 57 | "INSERT INTO assets(name,data,sha256) VALUES (?1,?2,?3)", |
| 58 | params![key, &bytes, &Sha256::digest(&bytes)[..]], |
| 59 | )?; |
| 60 | } |
| 61 | transaction.commit()?; |
| 62 | Ok(bytes) |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | pub(crate) fn key(filename: &str) -> Result<String> { |
| 67 | format!("<file>{filename}").parse::<FileDataReference>()?; |
| 68 | Ok(filename.to_ascii_lowercase()) |
| 69 | } |
| 70 | |
| 71 | impl Replica { |
| 72 | /// Whether the local pages or the remote image declare the external payload `key`. |
| 73 | fn references(&self, key: &str) -> Result<bool> { |
| 74 | for image in [self.snapshot()?, self.remote_snapshot()?] { |
| 75 | let store = Store::parse(&image)?; |
| 76 | let index = RevisionIndex::parse(&store)?; |
| 77 | let document = Document::parse(&index)?; |
| 78 | if document.spaces.values().flat_map(|space| space.revisions.values()) |
| 79 | .flat_map(|revision| revision.nodes.values()).any(|node| { |
| 80 | matches!(&node.kind, Kind::File { reference: FileDataReference::External(name), .. } if name.eq_ignore_ascii_case(key)) |
| 81 | }) { |
| 82 | return Ok(true); |
| 83 | } |
| 84 | } |
| 85 | Ok(false) |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | pub(crate) fn cached(connection: &Connection, key: &str, limit: usize) -> Result<Option<Vec<u8>>> { |
| 90 | let length: Option<i64> = connection |
| 91 | .query_row( |
| 92 | "SELECT length(data) FROM assets WHERE name=?1", |
| 93 | [key], |
| 94 | |row| row.get(0), |
| 95 | ) |
| 96 | .optional()?; |
| 97 | let Some(length) = length else { |
| 98 | return Ok(None); |
| 99 | }; |
| 100 | let length = |
| 101 | usize::try_from(length).map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; |
| 102 | if length > limit { |
| 103 | return Err(io::Error::from(io::ErrorKind::FileTooLarge).into()); |
| 104 | } |
| 105 | let (bytes, expected): (Vec<u8>, Vec<u8>) = connection.query_row( |
| 106 | "SELECT data,sha256 FROM assets WHERE name=?1", |
| 107 | [key], |
| 108 | |row| Ok((row.get(0)?, row.get(1)?)), |
| 109 | )?; |
| 110 | if bytes.len() != length || Sha256::digest(&bytes)[..] != expected { |
| 111 | return Err(io::Error::new( |
| 112 | io::ErrorKind::InvalidData, |
| 113 | "Cached external payload checksum mismatch", |
| 114 | ) |
| 115 | .into()); |
| 116 | } |
| 117 | Ok(Some(bytes)) |
| 118 | } |