use super::FileStorage;
use crate::engine::provisioning::paths::PayloadPaths;
use crate::engine::{core::SharedBlock, storage::PieceSet};
use crate::protocol::metainfo::{FileKind, OwnedMetainfo};
use std::{
fs::File,
io,
os::unix::fs::{FileExt, MetadataExt},
};
const FINGERPRINT_BYTES: usize = 40;
struct Extent {
offset: u64,
length: u64,
file: Option<File>,
}
pub(super) struct Files {
entries: Vec<Extent>,
payload_root: Option<File>,
paths: Option<PayloadPaths>,
}
impl Files {
fn span(&self, start: u64, end: u64) -> std::ops::Range<usize> {
let first = self
.entries
.partition_point(|e| e.offset + e.length <= start);
let last = first + self.entries[first..].partition_point(|e| e.offset < end);
first..last
}
}
impl FileStorage {
pub(in crate::engine) fn with_binding(
mut self,
binding: crate::engine::provisioning::DestinationBinding,
resume: Option<Vec<u8>>,
) -> Self {
self.binding = Some(binding);
self.verified_resume = resume;
self
}
pub fn binding(&self) -> &crate::engine::provisioning::DestinationBinding {
self.binding.as_ref().expect("provisioned destination")
}
pub fn verified_resume(&self) -> Option<&[u8]> {
self.verified_resume.as_deref()
}
pub(super) fn read_only(meta: OwnedMetainfo, files: Vec<Option<File>>) -> io::Result<Self> {
Self::new(meta, files, true)
}
pub(in crate::engine) fn named(
meta: OwnedMetainfo,
files: Vec<Option<File>>,
root: File,
) -> io::Result<Self> {
let paths = PayloadPaths::new(&meta)?;
let mut storage = Self::new(meta, files, false)?;
storage.files.payload_root = Some(root);
storage.files.paths = Some(paths);
Ok(storage)
}
pub(in crate::engine) fn new(
meta: OwnedMetainfo,
files: Vec<Option<File>>,
read_only: bool,
) -> io::Result<Self> {
if files.len() != meta.layout().files().len() {
return Err(invalid("file count mismatch"));
}
let mut entries = Vec::with_capacity(files.len());
let mut offset = 0;
for (entry, file) in meta.layout().files().iter().zip(files) {
if file.is_some() != (entry.kind() == FileKind::Data) {
return Err(invalid("file/padding mismatch"));
}
if let Some(file) = &file {
let stat = file.metadata()?;
if !stat.is_file() || stat.len() != entry.length() {
return Err(invalid("storage requires exact-sized regular files"));
}
}
entries.push(Extent {
offset,
length: entry.length(),
file,
});
offset += entry.length();
}
Ok(Self {
meta,
files: Files {
entries,
payload_root: None,
paths: None,
},
read_only,
binding: None,
verified_resume: None,
})
}
pub(in crate::engine) fn fingerprint_files<'a>(
files: impl Iterator<Item = Option<&'a File>>,
) -> io::Result<Vec<u8>> {
let mut out = Vec::new();
for file in files {
if let Some(file) = file {
let m = file.metadata()?;
out.extend_from_slice(&m.len().to_le_bytes());
out.extend_from_slice(&m.mtime().to_le_bytes());
out.extend_from_slice(&m.mtime_nsec().to_le_bytes());
out.extend_from_slice(&m.dev().to_le_bytes());
out.extend_from_slice(&m.ino().to_le_bytes());
} else {
out.extend_from_slice(&[0; FINGERPRINT_BYTES]);
}
}
Ok(out)
}
fn range(&self, piece: u32, offset: u32, len: usize) -> io::Result<u64> {
let range = self
.meta
.layout()
.piece_range(piece)
.ok_or_else(|| invalid("invalid piece"))?;
if u64::from(offset)
.checked_add(len as u64)
.is_none_or(|end| end > range.end - range.start)
{
return Err(invalid("range crosses piece boundary"));
}
Ok(range.start + u64::from(offset))
}
pub(in crate::engine) fn write_batch(
&self,
blocks: Vec<(u32, u32, SharedBlock)>,
) -> io::Result<()> {
if self.read_only {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"read-only storage",
));
}
let blocks = blocks
.into_iter()
.map(|(piece, offset, bytes)| {
self.range(piece, offset, bytes.len())
.map(|start| (start, bytes))
})
.collect::<io::Result<Vec<_>>>()?;
for (start, data) in blocks {
let end = start + data.len() as u64;
let span = self.files.span(start, end);
for e in &self.files.entries[span.clone()] {
let a = start.max(e.offset);
let b = end.min(e.offset + e.length);
if a < b
&& e.file.is_none()
&& data[(a - start) as usize..(b - start) as usize]
.iter()
.any(|&b| b != 0)
{
return Err(invalid("nonzero padding"));
}
}
for e in &self.files.entries[span] {
let a = start.max(e.offset);
let b = end.min(e.offset + e.length);
if a < b
&& let Some(file) = &e.file
{
file.write_all_at(
&data[(a - start) as usize..(b - start) as usize],
a - e.offset,
)?;
}
}
}
Ok(())
}
pub(in crate::engine) fn read(
&self,
piece: u32,
offset: u32,
length: usize,
) -> io::Result<Vec<u8>> {
let start = self.range(piece, offset, length)?;
let end = start + length as u64;
let mut bytes = vec![0; length];
for e in &self.files.entries[self.files.span(start, end)] {
let a = start.max(e.offset);
let b = end.min(e.offset + e.length);
if a < b
&& let Some(file) = &e.file
{
file.read_exact_at(
&mut bytes[(a - start) as usize..(b - start) as usize],
a - e.offset,
)?;
}
}
Ok(bytes)
}
pub(in crate::engine) fn finish_file(&self, file: u32) -> io::Result<()> {
if file as usize >= self.meta.layout().files().len() {
return Err(invalid("invalid file"));
}
if self.read_only || self.meta.layout().files()[file as usize].kind() != FileKind::Data {
return Ok(());
}
if let Some(paths) = &self.files.paths
&& let Some(root) = &self.files.payload_root
{
paths.finish_at(root, file)?;
}
Ok(())
}
pub(in crate::engine) fn sync_data(&self) -> io::Result<()> {
if !self.read_only {
for entry in &self.files.entries {
if let Some(file) = &entry.file {
file.sync_data()?;
}
}
}
Ok(())
}
pub(in crate::engine) fn fingerprint(&self) -> io::Result<Vec<u8>> {
Self::fingerprint_files(self.files.entries.iter().map(|e| e.file.as_ref()))
}
pub(in crate::engine) fn changed_since(&self, old: &[u8]) -> io::Result<PieceSet> {
let now = self.fingerprint()?;
if old.len() != now.len() {
return Ok((0..self.meta.layout().piece_count()).collect());
}
let mut changed = vec![false; self.meta.layout().piece_count() as usize];
let mut offset = 0;
for (i, file) in self.meta.layout().files().iter().enumerate() {
let start = i * FINGERPRINT_BYTES;
let end = start + FINGERPRINT_BYTES;
if file.length() > 0 && now[start..end] != old[start..end] {
let first = offset / u64::from(self.meta.layout().piece_length());
let last =
(offset + file.length() - 1) / u64::from(self.meta.layout().piece_length());
changed[first as usize..=last as usize].fill(true);
}
offset += file.length();
}
Ok(changed
.iter()
.enumerate()
.filter_map(|(i, &b)| b.then_some(i as u32))
.collect())
}
}
fn invalid(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message)
}