blob: be78f2a8c7b8f1b9cabc5d59d93ba714498666ae [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{
BundleError, BundleSummary, Inventory,
format::{Blob, CHUNK, MAGIC, Record},
};
use crate::StoreError;
use sha2::{Digest, Sha256};
use std::{io, time::Duration};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
/// Decode a complete transfer for preview. Import must separately validate its
/// original canonical proofs and destination authority before publication.
pub async fn inspect<R: AsyncRead + Unpin>(input: R) -> Result<BundleSummary, BundleError> {
let mut reader = Reader::new(input).await?;
loop {
match reader.next().await? {
Record::Blob(_) => reader.copy_blob(&mut tokio::io::sink()).await?,
Record::End { .. } => return Ok(reader.summary()?),
_ => {}
}
}
}
fn validate_inventory(inventory: &Inventory) -> Result<(), StoreError> {
if inventory.sessions.is_empty() {
return Err(StoreError::PrefixTooLarge);
}
let mut digest = Sha256::new();
let mut previous = None::<&str>;
let mut root = false;
for session in &inventory.sessions {
crate::sessions::validate_id(&session.id)?;
if session.revision == 0
|| session.revision > i64::MAX as u64
|| previous.is_some_and(|id| id >= session.id.as_str())
{
return Err(invalid(
"bundle inventory is not unique, sorted and revisioned",
));
}
if previous.is_some() {
digest.update(b"\n");
}
digest.update(session.id.as_bytes());
previous = Some(&session.id);
root |= session.id == inventory.root_session_id;
}
if !root || hex::encode(digest.finalize()) != inventory.subtree_digest {
return Err(invalid("bundle inventory digest or root differs"));
}
Ok(())
}
pub(super) struct Reader<R> {
input: R,
digest: Sha256,
bytes: u64,
frames: u64,
pending: Option<BlobRead>,
finished: Option<String>,
inventory: Option<Inventory>,
fence: u64,
previous_event: u64,
}
struct BlobRead {
descriptor: Blob,
remaining: u64,
digest: Sha256,
}
impl<R: AsyncRead + Unpin> Reader<R> {
pub async fn new(input: R) -> Result<Self, StoreError> {
let mut reader = Self {
input,
digest: Sha256::new(),
bytes: 0,
frames: 0,
pending: None,
finished: None,
inventory: None,
fence: 0,
previous_event: 0,
};
let mut magic = vec![0; MAGIC.len()];
reader.read(&mut magic).await?;
if magic != MAGIC {
return Err(invalid("unsupported Session bundle"));
}
Ok(reader)
}
pub async fn next(&mut self) -> Result<Record, StoreError> {
if self.finished.is_some() || self.pending.is_some() {
return Err(invalid("bundle frame boundary violated"));
}
let before = format!("sha256:{}", hex::encode(self.digest.clone().finalize()));
let mut length = [0; 4];
self.read(&mut length).await?;
let length = u32::from_be_bytes(length) as usize;
if length == 0 {
return Err(StoreError::PrefixTooLarge);
}
// A declared length is framing, not evidence that any payload exists.
// Grow only after consuming real bytes, never allocate the untrusted u32.
let mut bytes = Vec::new();
// Keep transport scratch off the enclosing async future's stack frame.
let mut chunk = Vec::new();
chunk.try_reserve_exact(CHUNK).map_err(io::Error::other)?;
chunk.resize(CHUNK, 0);
while bytes.len() < length {
let count = (length - bytes.len()).min(CHUNK);
self.read(&mut chunk[..count]).await?;
bytes.try_reserve(count).map_err(io::Error::other)?;
bytes.extend_from_slice(&chunk[..count]);
}
let record: Record = serde_json::from_slice(&bytes)?;
if self.frames == 0 && !matches!(record, Record::Header { .. }) {
return Err(invalid("bundle must start with its inventory"));
}
match &record {
Record::Header {
inventory,
source_high_water,
} => {
if self.frames != 0 {
return Err(invalid("duplicate bundle header"));
}
validate_inventory(inventory)?;
if *source_high_water > i64::MAX as u64 {
return Err(invalid("bundle source fence is out of range"));
}
self.inventory = Some(inventory.clone());
self.fence = *source_high_water;
}
Record::Event { sequence, .. } => {
if *sequence <= self.previous_event || *sequence > self.fence {
return Err(invalid(
"bundle events are not ordered within their source fence",
));
}
self.previous_event = *sequence;
}
Record::Blob(blob) => {
self.pending = Some(BlobRead {
descriptor: blob.clone(),
remaining: blob.bytes(),
digest: Sha256::new(),
});
}
Record::End { digest, frames } => {
if *frames != self.frames || *digest != before {
return Err(invalid("bundle footer does not match its bytes"));
}
if tokio::time::timeout(Duration::from_secs(30), self.input.read(&mut [0; 1]))
.await
.map_err(stalled)??
!= 0
{
return Err(invalid("trailing bytes after bundle footer"));
}
self.finished = Some(digest.clone());
}
_ => {}
}
self.frames = self
.frames
.checked_add(1)
.ok_or(StoreError::PrefixTooLarge)?;
Ok(record)
}
pub async fn copy_blob<W: AsyncWrite + Unpin>(
&mut self,
output: &mut W,
) -> Result<(), StoreError> {
while let Some(bytes) = self.blob_chunk().await? {
super::format::write_progress(output, &bytes).await?;
}
Ok(())
}
pub async fn blob_chunk(&mut self) -> Result<Option<Vec<u8>>, StoreError> {
let remaining = self
.pending
.as_ref()
.ok_or_else(|| invalid("no pending bundle payload"))?
.remaining;
if remaining == 0 {
let pending = self.pending.take().expect("pending blob");
if format!("sha256:{}", hex::encode(pending.digest.finalize()))
!= pending.descriptor.digest()
{
return Err(invalid("bundle payload digest mismatch"));
}
return Ok(None);
}
let mut bytes = vec![0; remaining.min(CHUNK as u64) as usize];
self.read(&mut bytes).await?;
let pending = self.pending.as_mut().expect("pending blob");
pending.digest.update(&bytes);
pending.remaining -= bytes.len() as u64;
Ok(Some(bytes))
}
pub fn summary(self) -> Result<BundleSummary, StoreError> {
Ok(BundleSummary {
inventory: self
.inventory
.ok_or_else(|| invalid("bundle header missing"))?,
bytes: self.bytes,
digest: self
.finished
.ok_or_else(|| invalid("bundle footer missing"))?,
})
}
async fn read(&mut self, bytes: &mut [u8]) -> Result<(), StoreError> {
let mut offset = 0;
while offset < bytes.len() {
let count = tokio::time::timeout(
Duration::from_secs(30),
self.input.read(&mut bytes[offset..]),
)
.await
.map_err(stalled)??;
if count == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"bundle ended within a frame",
)
.into());
}
offset += count;
}
self.digest.update(&*bytes);
self.bytes = self
.bytes
.checked_add(bytes.len() as u64)
.ok_or(StoreError::PrefixTooLarge)?;
Ok(())
}
}
fn stalled(_: tokio::time::error::Elapsed) -> io::Error {
io::Error::new(io::ErrorKind::TimedOut, "Session bundle transfer stalled")
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}