use crate::pb::ParsingFailed;
use crate::{InvalidCidInLink, Metadata, UnexpectedNodeType};
use std::borrow::Cow;
use std::fmt;
pub mod reader;
pub mod visit;
#[derive(Debug)]
pub enum FileReadFailed {
File(FileError),
UnexpectedType(UnexpectedNodeType),
Read(Option<quick_protobuf::Error>),
InvalidCid(InvalidCidInLink),
}
impl fmt::Display for FileReadFailed {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use FileReadFailed::*;
match self {
File(e) => write!(fmt, "{}", e),
UnexpectedType(ut) => write!(fmt, "unexpected type for UnixFs: {:?}", ut),
Read(Some(e)) => write!(fmt, "reading failed: {}", e),
Read(None) => write!(fmt, "reading failed: missing UnixFS message"),
InvalidCid(e) => write!(fmt, "{}", e),
}
}
}
impl std::error::Error for FileReadFailed {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use FileReadFailed::*;
match self {
InvalidCid(e) => Some(e),
Read(Some(e)) => Some(e),
_ => None,
}
}
}
impl<'a> From<ParsingFailed<'a>> for FileReadFailed {
fn from(e: ParsingFailed<'a>) -> Self {
use ParsingFailed::*;
match e {
InvalidDagPb(e) => FileReadFailed::Read(Some(e)),
InvalidUnixFs(e, _) => FileReadFailed::Read(Some(e)),
NoData(_) => FileReadFailed::Read(None),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum FileError {
LinksAndBlocksizesMismatch,
NoLinksNoContent,
NonRootDefinesMetadata(Metadata),
IntermediateNodeWithoutFileSize,
TreeExpandsOnLinks,
TreeOverlapsBetweenLinks,
EarlierLink,
TreeJumpsBetweenLinks,
UnexpectedRawOrFileProperties {
hash_type: Option<u64>,
fanout: Option<u64>,
},
}
impl fmt::Display for FileError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
use FileError::*;
match self {
LinksAndBlocksizesMismatch => write!(
fmt,
"different number of links and blocksizes: cannot determine subtree ranges"
),
NoLinksNoContent => write!(
fmt,
"filesize is non-zero while there are no links or content"
),
NonRootDefinesMetadata(metadata) => {
write!(fmt, "unsupported: non-root defines {:?}", metadata)
}
IntermediateNodeWithoutFileSize => {
write!(fmt, "intermediatery node with links but no filesize")
}
TreeExpandsOnLinks => write!(
fmt,
"total size of tree expands through links, it should only get smaller or keep size"
),
TreeOverlapsBetweenLinks => write!(fmt, "unsupported: tree contains overlap"),
EarlierLink => write!(fmt, "error: earlier link given"),
TreeJumpsBetweenLinks => write!(fmt, "unsupported: tree contains holes"),
UnexpectedRawOrFileProperties { hash_type, fanout } => write!(
fmt,
"unsupported: File or Raw with hash_type {:?} or fanount {:?}",
hash_type, fanout
),
}
}
}
impl std::error::Error for FileError {}
impl From<FileError> for FileReadFailed {
fn from(e: FileError) -> Self {
Self::File(e)
}
}
pub(crate) trait UnwrapBorrowedExt<'a> {
fn unwrap_borrowed(self) -> &'a [u8];
fn unwrap_borrowed_or_empty(self) -> &'a [u8]
where
Self: 'a;
}
impl<'a> UnwrapBorrowedExt<'a> for Option<Cow<'a, [u8]>> {
fn unwrap_borrowed(self) -> &'a [u8] {
match self {
Some(Cow::Borrowed(x)) => x,
Some(Cow::Owned(_)) => panic!("unexpected Cow::Owned"),
None => panic!("Unexpected None"),
}
}
fn unwrap_borrowed_or_empty(self) -> &'a [u8] {
match self {
Some(Cow::Borrowed(x)) => x,
None => &[][..],
_ => panic!("should not be Cow::Owned"),
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::{reader::*, visit::*, UnwrapBorrowedExt};
use crate::test_support::FakeBlockstore;
use hex_literal::hex;
const CONTENT_FILE: &[u8] = &hex!("0a0d08021207636f6e74656e741807");
#[test]
fn just_content() {
let fr = FileReader::from_block(CONTENT_FILE).unwrap();
let (content, _) = fr.content();
assert!(
matches!(content, FileContent::Bytes(b"content")),
"{:?}",
content
);
}
#[test]
fn visiting_just_content() {
let res = IdleFileVisit::default().start(CONTENT_FILE);
assert!(matches!(res, Ok((b"content", _, _, None))), "{:?}", res);
}
#[test]
fn visiting_too_large_range_of_singleblock_file() {
let res = IdleFileVisit::default()
.with_target_range(500_000..600_000)
.start(CONTENT_FILE);
assert!(matches!(res, Ok((b"", _, _, None))), "{:?}", res);
}
#[test]
fn empty_file() {
let block = &hex!("0a0408021800");
let fr = FileReader::from_block(block).unwrap();
let (content, _) = fr.content();
assert!(matches!(content, FileContent::Bytes(b"")), "{:?}", content);
}
#[test]
fn balanced_traversal() {
let target = "QmRJHYTNvC3hmd9gJQARxLR1QMEincccBV53bBw524yyq6";
let blocks = FakeBlockstore::with_fixtures();
let (mut links_and_ranges, mut traversal) = {
let root = FileReader::from_block(blocks.get_by_str(target)).unwrap();
let (mut links_and_ranges, traversal) = match root.content() {
(FileContent::Links(iter), traversal) => {
let links_and_ranges = iter
.map(|(link, range)| (link.Hash.unwrap_borrowed().to_vec(), range))
.collect::<Vec<_>>();
(links_and_ranges, traversal)
}
x => unreachable!("unexpected {:?}", x),
};
links_and_ranges.reverse();
(links_and_ranges, traversal)
};
let mut combined: Vec<u8> = Vec::new();
while let Some((key, range)) = links_and_ranges.pop() {
let next = blocks.get_by_raw(&key);
let fr = traversal.continue_walk(&next, &range).unwrap();
let (content, next) = fr.content();
combined.extend(content.unwrap_content());
traversal = next;
}
assert_eq!(combined, b"foobar\n");
}
fn collect_bytes(blocks: &FakeBlockstore, visit: IdleFileVisit, start: &str) -> Vec<u8> {
let mut ret = Vec::new();
let (content, _, _, mut step) = visit.start(blocks.get_by_str(start)).unwrap();
ret.extend(content);
while let Some(visit) = step {
let (first, _) = visit.pending_links();
let block = blocks.get_by_cid(first);
let (content, next_step) = visit.continue_walk(block, &mut None).unwrap();
ret.extend(content);
step = next_step;
}
ret
}
#[test]
fn visitor_traversal() {
let blocks = FakeBlockstore::with_fixtures();
let start = "QmRJHYTNvC3hmd9gJQARxLR1QMEincccBV53bBw524yyq6";
let bytes = collect_bytes(&blocks, IdleFileVisit::default(), start);
assert_eq!(&bytes[..], b"foobar\n");
}
#[test]
fn scoped_visitor_traversal_from_blockstore() {
let blocks = FakeBlockstore::with_fixtures();
let start = "QmRJHYTNvC3hmd9gJQARxLR1QMEincccBV53bBw524yyq6";
let visit = IdleFileVisit::default().with_target_range(1..6);
let bytes = collect_bytes(&blocks, visit, start);
assert_eq!(&bytes[..], b"oobar");
}
#[test]
fn less_than_block_scoped_traversal_from_blockstore() {
let blocks = FakeBlockstore::with_fixtures();
let start = "QmRJHYTNvC3hmd9gJQARxLR1QMEincccBV53bBw524yyq6";
let visit = IdleFileVisit::default().with_target_range(0..1);
let bytes = collect_bytes(&blocks, visit, start);
assert_eq!(&bytes[..], b"f");
}
#[test]
fn scoped_traversal_out_of_bounds_from_blockstore() {
let blocks = FakeBlockstore::with_fixtures();
let start = "QmRJHYTNvC3hmd9gJQARxLR1QMEincccBV53bBw524yyq6";
let visit = IdleFileVisit::default().with_target_range(7..20);
let bytes = collect_bytes(&blocks, visit, start);
assert_eq!(&bytes[..], b"");
}
#[test]
fn trickle_traversal() {
let blocks = FakeBlockstore::with_fixtures();
let start = "QmWfQ48ChJUj4vWKFsUDe4646xCBmXgdmNfhjz9T7crywd";
let bytes = collect_bytes(&blocks, IdleFileVisit::default(), start);
assert_eq!(&bytes[..], b"foobar\n");
}
}