use std::io::{Read, Seek, SeekFrom, Write};
use super::{HistorySource, SourceRefusal};
use crate::core::context_admission::cross_line;
use crate::core::rooted_read::RootedUnitSource;
pub(crate) const MAX_TRANSCRIPT_BYTES: u64 = 512 * 1024 * 1024;
const READ_CHUNK_BYTES: usize = 64 * 1024;
pub(super) const SEAM_SIDE_BYTES: usize = 16 * cross_line::BOUNDED_REACH_BYTES;
pub(super) fn unit_bytes() -> usize {
crate::core::limits::max_read_bytes()
.min(crate::core::policy::content::MAX_PROTECTED_CONTENT_BYTES)
}
pub(super) struct AdmittedTranscript {
spool: std::fs::File,
units: Vec<usize>,
reader: RootedUnitSource,
}
#[cfg(test)]
thread_local! {
pub(super) static FAIL_BEFORE_UNIT: std::cell::Cell<Option<usize>> =
const { std::cell::Cell::new(None) };
}
#[cfg(test)]
fn spool_fault_before(unit: usize) -> bool {
FAIL_BEFORE_UNIT.with(std::cell::Cell::get) == Some(unit)
}
#[cfg(not(test))]
const fn spool_fault_before(_unit: usize) -> bool {
false
}
impl AdmittedTranscript {
pub(super) fn for_each_unit(self, mut each: impl FnMut(&str)) -> Result<(), SourceRefusal> {
let Self {
mut spool,
units,
reader,
} = self;
spool
.seek(SeekFrom::Start(0))
.map_err(|_| SourceRefusal::Unreadable)?;
for (index, length) in units.into_iter().enumerate() {
if spool_fault_before(index) {
return Err(SourceRefusal::Unreadable);
}
let mut bytes = vec![0_u8; length];
spool
.read_exact(&mut bytes)
.map_err(|_| SourceRefusal::Unreadable)?;
let text = String::from_utf8(bytes).map_err(|_| SourceRefusal::Unreadable)?;
each(&text);
}
if reader.gateway_rules_unchanged() {
Ok(())
} else {
Err(SourceRefusal::Withheld)
}
}
}
struct Admitting {
spool: std::fs::File,
units: Vec<usize>,
digest: blake3::Hasher,
written: u64,
tail: Vec<u8>,
seam_side: usize,
}
pub(super) fn admit(
source: &HistorySource,
gateway: &super::staging::Gateway,
) -> Result<AdmittedTranscript, SourceRefusal> {
let unit_cap = unit_bytes();
let mut reader = crate::core::rooted_read::open_rooted_unit_source(
&source.path.to_string_lossy(),
&source.root.to_string_lossy(),
"ctx_knowledge",
gateway.clone(),
)
.map_err(|error| refusal(&error))?;
if reader.byte_len() > MAX_TRANSCRIPT_BYTES {
return Err(SourceRefusal::TooLarge);
}
let spool = super::scratch_parent()
.and_then(tempfile::tempfile_in)
.map_err(|_| SourceRefusal::Unreadable)?;
let mut admitting = Admitting {
spool,
units: Vec::new(),
digest: blake3::Hasher::new(),
written: 0,
tail: Vec::new(),
seam_side: SEAM_SIDE_BYTES.min(unit_cap / 4),
};
let mut unit = Vec::new();
let mut record = Vec::new();
let mut chunk = vec![0_u8; READ_CHUNK_BYTES];
let mut total = 0_u64;
loop {
let read = reader
.read_raw(&mut chunk)
.map_err(|_| SourceRefusal::Unreadable)?;
if read == 0 {
break;
}
total += read as u64;
if total > MAX_TRANSCRIPT_BYTES {
return Err(SourceRefusal::TooLarge);
}
let mut rest = &chunk[..read];
while let Some(newline) = rest.iter().position(|byte| *byte == b'\n') {
let (head, tail) = rest.split_at(newline + 1);
extend_record(&mut record, head, unit_cap)?;
close_record(&reader, &mut unit, &mut record, unit_cap, &mut admitting)?;
rest = tail;
}
extend_record(&mut record, rest, unit_cap)?;
}
close_record(&reader, &mut unit, &mut record, unit_cap, &mut admitting)?;
admit_unit(&reader, &mut unit, &mut admitting)?;
verified(admitting, reader)
}
fn extend_record(record: &mut Vec<u8>, bytes: &[u8], cap: usize) -> Result<(), SourceRefusal> {
if record.len() + bytes.len() > cap {
return Err(SourceRefusal::RecordTooLarge);
}
record.extend_from_slice(bytes);
Ok(())
}
fn close_record(
reader: &RootedUnitSource,
unit: &mut Vec<u8>,
record: &mut Vec<u8>,
cap: usize,
admitting: &mut Admitting,
) -> Result<(), SourceRefusal> {
if unit.len() + record.len() > cap {
admit_unit(reader, unit, admitting)?;
}
unit.append(record);
Ok(())
}
fn admit_unit(
reader: &RootedUnitSource,
unit: &mut Vec<u8>,
admitting: &mut Admitting,
) -> Result<(), SourceRefusal> {
if unit.is_empty() {
return Ok(());
}
let bytes = std::mem::take(unit);
if !admitting.tail.is_empty() {
let mut seam = std::mem::take(&mut admitting.tail);
seam.extend_from_slice(seam_head(&bytes, admitting.seam_side));
let seam = reader.admit_seam(seam).map_err(|error| refusal(&error))?;
if !seam.storable {
return Err(SourceRefusal::Withheld);
}
}
admitting.tail = seam_tail(&bytes, admitting.seam_side).to_vec();
let unit = reader.admit_unit(bytes).map_err(|error| refusal(&error))?;
if !unit.storable {
return Err(SourceRefusal::Withheld);
}
admitting
.spool
.write_all(unit.content.as_bytes())
.map_err(|_| SourceRefusal::Unreadable)?;
admitting.digest.update(unit.content.as_bytes());
admitting.written += unit.content.len() as u64;
admitting.units.push(unit.content.len());
Ok(())
}
fn seam_tail(unit: &[u8], side: usize) -> &[u8] {
if unit.len() <= side {
return unit;
}
let start = unit.len() - side;
let record = unit[..start]
.iter()
.rposition(|byte| *byte == b'\n')
.map_or(0, |newline| newline + 1);
if unit.len() - record <= 2 * side {
return &unit[record..];
}
let mut start = start;
while start < unit.len() && unit[start] & 0xC0 == 0x80 {
start += 1;
}
&unit[start..]
}
fn seam_head(unit: &[u8], side: usize) -> &[u8] {
if unit.len() <= side {
return unit;
}
let record = unit[side - 1..]
.iter()
.position(|byte| *byte == b'\n')
.map_or(unit.len(), |newline| side + newline);
if record <= 2 * side {
return &unit[..record];
}
let mut end = side;
while end > 0 && unit[end] & 0xC0 == 0x80 {
end -= 1;
}
&unit[..end]
}
fn verified(
admitting: Admitting,
reader: RootedUnitSource,
) -> Result<AdmittedTranscript, SourceRefusal> {
let Admitting {
mut spool,
units,
digest,
written,
..
} = admitting;
spool
.seek(SeekFrom::Start(0))
.map_err(|_| SourceRefusal::Unreadable)?;
let mut read_back = blake3::Hasher::new();
let mut length = 0_u64;
let mut chunk = vec![0_u8; READ_CHUNK_BYTES];
loop {
let read = spool
.read(&mut chunk)
.map_err(|_| SourceRefusal::Unreadable)?;
if read == 0 {
break;
}
read_back.update(&chunk[..read]);
length += read as u64;
}
if length != written || read_back.finalize() != digest.finalize() {
return Err(SourceRefusal::Unreadable);
}
Ok(AdmittedTranscript {
spool,
units,
reader,
})
}
fn refusal(error: &std::io::Error) -> SourceRefusal {
match error.kind() {
std::io::ErrorKind::Unsupported => SourceRefusal::UnitAdmissionUnsupported,
std::io::ErrorKind::PermissionDenied => SourceRefusal::Withheld,
_ => SourceRefusal::Unreadable,
}
}