1use crate::error::{Error, Result};
23use crate::file::OpenedFile;
24use crate::header::FixedHeader;
25use crate::payload::read_range;
26use crate::tlv::CompressionIndexEntry;
27use std::io::{Read, Seek, SeekFrom};
28use std::path::{Path, PathBuf};
29
30pub trait BlockSource {
35 fn header(&self) -> &FixedHeader;
37
38 fn read_ct(&self, offset: u64, len: u64) -> Result<Vec<u8>>;
46
47 fn ct_len(&self) -> u64;
49
50 fn is_remote(&self) -> bool {
52 false
53 }
54
55 fn describe(&self) -> String {
57 String::from("<block source>")
58 }
59}
60
61#[derive(Debug)]
63pub struct MemorySource {
64 header: FixedHeader,
65 payload: Vec<u8>,
67}
68
69impl MemorySource {
70 pub fn new(file_bytes: &[u8]) -> Result<Self> {
76 let header = crate::file::peek_header(file_bytes)?;
77 let start = usize::try_from(header.header_len)
78 .map_err(|_| Error::MalformedHeader { reason: "header_len exceeds usize" })?;
79 let payload = file_bytes
80 .get(start..)
81 .ok_or(Error::Truncated {
82 context: "payload area",
83 need: start,
84 got: file_bytes.len(),
85 })?
86 .to_vec();
87 Ok(Self { header, payload })
88 }
89}
90
91impl BlockSource for MemorySource {
92 fn header(&self) -> &FixedHeader {
93 &self.header
94 }
95
96 fn read_ct(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
97 let start = usize::try_from(offset)
98 .map_err(|_| Error::ChunkOutOfRange { index: offset, total: self.ct_len() })?;
99 let n = usize::try_from(len)
100 .map_err(|_| Error::ChunkOutOfRange { index: len, total: self.ct_len() })?;
101 let end = start
102 .checked_add(n)
103 .ok_or(Error::ChunkOutOfRange { index: offset, total: self.ct_len() })?;
104 self.payload
105 .get(start..end)
106 .map(<[u8]>::to_vec)
107 .ok_or(Error::Truncated {
108 context: "memory ciphertext range",
109 need: end,
110 got: self.payload.len(),
111 })
112 }
113
114 fn ct_len(&self) -> u64 {
115 self.payload.len() as u64
116 }
117
118 fn describe(&self) -> String {
119 String::from("memory")
120 }
121}
122
123#[derive(Debug)]
127pub struct LocalFileSource {
128 path: PathBuf,
129 header: FixedHeader,
130 payload_start: u64,
131 payload_len: u64,
132}
133
134impl LocalFileSource {
135 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
143 let path = path.as_ref().to_path_buf();
144 let mut f = std::fs::File::open(&path)?;
145
146 let mut probe = vec![0u8; crate::scan::MIN_PROBE_SIZE];
149 let n = read_at_most(&mut f, &mut probe)?;
150 probe.truncate(n);
151 let partial = crate::file::peek_header(&probe)?;
152
153 let hlen = partial.header_len;
154 let total = f.metadata()?.len();
155 if total < u64::from(hlen) {
156 return Err(Error::Truncated {
157 context: "local file header",
158 need: hlen as usize,
159 got: usize::try_from(total).unwrap_or(usize::MAX),
160 });
161 }
162
163 let mut head = vec![0u8; hlen as usize];
165 f.seek(SeekFrom::Start(0))?;
166 f.read_exact(&mut head)?;
167 let header = crate::file::peek_header(&head)?;
168
169 Ok(Self {
170 path,
171 payload_start: u64::from(hlen),
172 payload_len: total.saturating_sub(u64::from(hlen)),
173 header,
174 })
175 }
176
177 #[must_use]
179 pub fn path(&self) -> &Path {
180 &self.path
181 }
182}
183
184impl BlockSource for LocalFileSource {
185 fn header(&self) -> &FixedHeader {
186 &self.header
187 }
188
189 fn read_ct(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
190 let end = offset
191 .checked_add(len)
192 .ok_or(Error::ChunkOutOfRange { index: offset, total: self.payload_len })?;
193 if end > self.payload_len {
194 return Err(Error::Truncated {
195 context: "local file ciphertext range",
196 need: usize::try_from(end).unwrap_or(usize::MAX),
197 got: usize::try_from(self.payload_len).unwrap_or(usize::MAX),
198 });
199 }
200 let n = usize::try_from(len)
201 .map_err(|_| Error::ChunkOutOfRange { index: len, total: self.payload_len })?;
202
203 let mut f = std::fs::File::open(&self.path)?;
204 f.seek(SeekFrom::Start(
205 self.payload_start
206 .checked_add(offset)
207 .ok_or(Error::ChunkOutOfRange { index: offset, total: self.payload_len })?,
208 ))?;
209 let mut buf = vec![0u8; n];
210 f.read_exact(&mut buf)?;
211 Ok(buf)
212 }
213
214 fn ct_len(&self) -> u64 {
215 self.payload_len
216 }
217
218 fn describe(&self) -> String {
219 self.path.display().to_string()
220 }
221}
222
223fn read_at_most(f: &mut std::fs::File, buf: &mut [u8]) -> Result<usize> {
225 let mut total = 0usize;
226 while total < buf.len() {
227 let Some(slice) = buf.get_mut(total..) else {
228 break;
229 };
230 match f.read(slice) {
231 Ok(0) => break,
232 Ok(n) => total = total.saturating_add(n),
233 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
234 Err(e) => return Err(e.into()),
235 }
236 }
237 Ok(total)
238}
239
240#[derive(Debug)]
246pub struct ContainerEntryView<'a, S: BlockSource> {
247 inner: &'a S,
248 base: u64,
249 len: u64,
250}
251
252impl<'a, S: BlockSource> ContainerEntryView<'a, S> {
253 #[must_use]
255 pub const fn new(inner: &'a S, offset: u64, size: u64) -> Self {
256 Self { inner, base: offset, len: size }
257 }
258
259 #[must_use]
261 pub const fn plaintext_len(&self) -> u64 {
262 self.len
263 }
264
265 pub fn read(
274 &self,
275 opened: &OpenedFile,
276 offset: u64,
277 length: u64,
278 ) -> Result<Vec<u8>> {
279 if offset >= self.len {
280 return Ok(Vec::new());
281 }
282 let avail = self.len.saturating_sub(offset);
283 let want = length.min(avail);
284 let abs = self
285 .base
286 .checked_add(offset)
287 .ok_or(Error::MalformedHeader { reason: "entry offset overflow" })?;
288 read_source_range(self.inner, opened, abs, want)
289 }
290}
291
292pub fn read_source_range<S: BlockSource + ?Sized>(
301 src: &S,
302 opened: &OpenedFile,
303 offset: u64,
304 length: u64,
305) -> Result<Vec<u8>> {
306 let index: Option<Vec<CompressionIndexEntry>> =
307 if src.header().has_flag(crate::header::flags::COMPRESSED) {
308 Some(opened.compression_index()?)
309 } else {
310 None
311 };
312
313 read_range(
314 src.header(),
315 opened.payload_key(),
316 index.as_deref(),
317 offset,
318 length,
319 |ct_off, ct_len| src.read_ct(ct_off, ct_len),
320 )
321}
322
323pub fn decrypt_to_sink<S: BlockSource + ?Sized>(
337 src: &S,
338 opened: &OpenedFile,
339 sink: &mut dyn FnMut(&[u8]) -> Result<()>,
340 mut progress: Option<crate::payload::ProgressFn<'_>>,
341) -> Result<u64> {
342 use blake2::digest::{Digest, consts::U32};
343
344 let total = opened.header.plaintext_size;
345 let chunk = u64::from(opened.header.chunk_size).max(1);
346 let window = crate::unpack::STREAM_WINDOW
348 .checked_div(chunk)
349 .unwrap_or(1)
350 .max(1)
351 .saturating_mul(chunk);
352 let index: Option<Vec<CompressionIndexEntry>> =
353 if src.header().has_flag(crate::header::flags::COMPRESSED) {
354 Some(opened.compression_index()?)
355 } else {
356 None
357 };
358 let mut hasher = blake2::Blake2b::<U32>::new();
359 let mut done = 0u64;
360 while done < total {
361 let n = total.saturating_sub(done).min(window);
362 let plain = read_range(
363 src.header(),
364 opened.payload_key(),
365 index.as_deref(),
366 done,
367 n,
368 |ct_off, ct_len| src.read_ct(ct_off, ct_len),
369 )?;
370 if plain.len() as u64 != n {
371 return Err(Error::Truncated {
372 context: "payload",
373 need: usize::try_from(n).unwrap_or(usize::MAX),
374 got: plain.len(),
375 });
376 }
377 hasher.update(&plain);
378 sink(&plain)?;
379 done = done.saturating_add(n);
380 if let Some(cb) = progress.as_deref_mut() {
381 cb(done, total);
382 }
383 }
384 if let Ok(expected) = opened.stored_content_hash() {
385 use subtle::ConstantTimeEq;
386 let actual: [u8; 32] = hasher.finalize().into();
387 if !bool::from(actual.ct_eq(&expected)) {
388 return Err(Error::ContentHashMismatch);
389 }
390 }
391 Ok(done)
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::crypto::{Argon2Params, Kek};
398 use crate::file::{EncryptOptions, RandomMaterial, encrypt, open};
399 use crate::header::MIN_CHUNK_SIZE;
400
401 fn make(payload: &[u8], compress: bool) -> (Vec<u8>, Kek) {
402 let salt = [0x22u8; 16];
403 let params = Argon2Params::TEST_WEAK;
404 let kek = Kek::from_password(b"src", &salt, params).unwrap();
405 let opts = EncryptOptions {
406 filename: Some("s.bin".into()),
407 chunk_size: MIN_CHUNK_SIZE,
408 argon2: params,
409 compress,
410 ..EncryptOptions::default()
411 };
412 let k2 = Kek::from_password(b"src", &salt, params).unwrap();
413 let enc = encrypt(payload, &[kek], &salt, &opts, &RandomMaterial::generate()).unwrap();
414 (enc.bytes, k2)
415 }
416
417 #[test]
418 fn memory_source_reads_ranges() {
419 let plain: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
420 let (bytes, kek) = make(&plain, false);
421 let src = MemorySource::new(&bytes).unwrap();
422 let opened = open(&bytes, &[kek]).unwrap();
423
424 for (off, len) in [(0u64, 10u64), (65_530, 20), (199_990, 10), (0, 200_000)] {
425 let got = read_source_range(&src, &opened, off, len).unwrap();
426 let want = &plain[off as usize..(off + len) as usize];
427 assert_eq!(got, want, "区间 ({off},{len}) 不符");
428 }
429 assert!(!src.is_remote());
430 }
431
432 #[test]
433 fn local_file_source_matches_memory() {
434 let plain: Vec<u8> = (0..150_000u32).map(|i| (i % 253) as u8).collect();
435 let (bytes, kek) = make(&plain, false);
436
437 let dir = std::env::temp_dir().join(format!("omy_src_{}", std::process::id()));
438 std::fs::create_dir_all(&dir).unwrap();
439 let p = dir.join("a.omy");
440 std::fs::write(&p, &bytes).unwrap();
441
442 let src = LocalFileSource::open(&p).unwrap();
443 let opened = open(&bytes, &[kek]).unwrap();
444
445 assert_eq!(src.ct_len(), (bytes.len() - src.header().header_len as usize) as u64);
446
447 for (off, len) in [(0u64, 100u64), (65_536, 1000), (149_000, 1000)] {
448 let got = read_source_range(&src, &opened, off, len).unwrap();
449 assert_eq!(got, &plain[off as usize..(off + len) as usize]);
450 }
451
452 std::fs::remove_dir_all(&dir).ok();
453 }
454
455 #[test]
456 fn compressed_source_reads_ranges() {
457 let plain = vec![0x5Au8; 300_000];
459 let (bytes, kek) = make(&plain, true);
460 let src = MemorySource::new(&bytes).unwrap();
461 let opened = open(&bytes, &[kek]).unwrap();
462 assert!(src.header().has_flag(crate::header::flags::COMPRESSED));
463
464 let got = read_source_range(&src, &opened, 100, 5000).unwrap();
465 assert_eq!(got, &plain[100..5100]);
466 }
467
468 #[test]
469 fn container_view_clips_to_entry() {
470 let mut plain = vec![0xAAu8; 1000];
472 plain.extend_from_slice(&[0xBBu8; 1000]);
473 let (bytes, kek) = make(&plain, false);
474 let src = MemorySource::new(&bytes).unwrap();
475 let opened = open(&bytes, &[kek]).unwrap();
476
477 let second = ContainerEntryView::new(&src, 1000, 1000);
478 assert_eq!(second.plaintext_len(), 1000);
479
480 let got = second.read(&opened, 0, 1000).unwrap();
482 assert_eq!(got.len(), 1000);
483 assert!(got.iter().all(|&b| b == 0xBB));
484
485 let got2 = second.read(&opened, 900, 500).unwrap();
487 assert_eq!(got2.len(), 100);
488 assert!(got2.iter().all(|&b| b == 0xBB));
489
490 assert!(second.read(&opened, 1000, 10).unwrap().is_empty());
492 }
493
494 #[test]
495 fn out_of_bounds_ct_read_errors() {
496 let (bytes, _) = make(b"short", false);
497 let src = MemorySource::new(&bytes).unwrap();
498 assert!(src.read_ct(0, src.ct_len() + 1).is_err());
499 assert!(src.read_ct(u64::MAX, 1).is_err());
500 }
501
502 #[test]
503 fn describe_is_useful() {
504 let (bytes, _) = make(b"x", false);
505 let src = MemorySource::new(&bytes).unwrap();
506 assert_eq!(src.describe(), "memory");
507 }
508
509 #[test]
514 fn decrypt_to_sink_matches_decrypt_all() -> Result<()> {
515 let big: Vec<u8> = (0..(crate::unpack::STREAM_WINDOW as usize * 2 + 12_345))
516 .map(|i| (i % 251) as u8)
517 .collect();
518 for compress in [false, true] {
519 let (file, kek) = make(&big, compress);
520 let opened = open(&file, &[kek])?;
521 let src = MemorySource::new(&file)?;
522 let mut out = Vec::new();
523 let n = decrypt_to_sink(&src, &opened, &mut |b| { out.extend_from_slice(b); Ok(()) }, None)?;
524 assert_eq!(n as usize, big.len());
525 assert_eq!(out, opened.decrypt_all(&file)?, "compress={compress}");
526 }
527 Ok(())
528 }
529}