use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::Path;
use sha2::{Digest, Sha256};
#[cfg(test)]
use super::fixtures::Hooks;
use super::types::{Entry, Error};
pub(super) enum Payload {
File(File),
Worker(File),
Bytes(Vec<u8>),
}
impl Payload {
pub(super) fn executable(path: &Path) -> Result<Self, Error> {
let file = regular(path, "bundle_source_invalid")?;
let metadata = file
.metadata()
.map_err(|error| Error::new("bundle_source_invalid", &error))?;
if metadata.len() == 0 || metadata.permissions().mode() & 0o111 == 0 {
return Err(Error::new(
"bundle_source_invalid",
"worker or operator is empty or not executable",
));
}
Ok(Self::File(file))
}
pub(super) fn worker(path: &Path) -> Result<Self, Error> {
match Self::executable(path)? {
Self::File(file) => Ok(Self::Worker(file)),
Self::Worker(_) | Self::Bytes(_) => Err(Error::new(
"bundle_source_invalid",
"worker must be an executable file",
)),
}
}
pub(super) fn entry(&mut self, mode: u32) -> Result<Entry, Error> {
let worker = matches!(self, Self::Worker(_));
let (bytes, sha256) = match self {
Self::File(file) | Self::Worker(file) => {
file.rewind()
.map_err(|error| Error::new("bundle_source_invalid", &error))?;
digest(file, worker)?
}
Self::Bytes(bytes) => (bytes.len() as u64, format!("{:x}", Sha256::digest(bytes))),
};
Ok(Entry {
bytes,
sha256,
mode,
})
}
pub(super) fn write(
&mut self,
path: &Path,
expected: &Entry,
#[cfg(test)] hooks: &mut Hooks,
) -> Result<(), Error> {
let mut target = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
.map_err(|error| Error::new("bundle_write_failed", &error))?;
match self {
Self::File(source) | Self::Worker(source) => {
source
.seek(SeekFrom::Start(0))
.map_err(|error| Error::new("bundle_source_changed", &error))?;
let limit = expected.bytes.checked_add(1).ok_or_else(|| {
Error::new("bundle_size_overflow", "payload copy bound overflowed")
})?;
std::io::copy(&mut source.take(limit), &mut target)
.map_err(|error| Error::new("bundle_write_failed", &error))?;
}
Self::Bytes(bytes) => target
.write_all(bytes)
.map_err(|error| Error::new("bundle_write_failed", &error))?,
}
target
.set_permissions(fs::Permissions::from_mode(expected.mode))
.map_err(|error| Error::new("bundle_write_failed", &error))?;
super::publication::sync_file(
&target,
path,
#[cfg(test)]
hooks,
)?;
let actual = entry(path, "bundle_source_changed")?;
if actual != *expected {
return Err(Error::new(
"bundle_source_changed",
"copied bytes or mode differ from the preflight",
));
}
Ok(())
}
}
pub(super) fn regular(path: &Path, code: &'static str) -> Result<File, Error> {
let before = fs::symlink_metadata(path).map_err(|error| Error::new(code, &error))?;
if !before.file_type().is_file() {
return Err(Error::new(
code,
"expected a regular file, not a symlink or directory",
));
}
let file = File::open(path).map_err(|error| Error::new(code, &error))?;
let opened = file.metadata().map_err(|error| Error::new(code, &error))?;
if before.dev() != opened.dev() || before.ino() != opened.ino() || !opened.is_file() {
return Err(Error::new(code, "file changed while it was opened"));
}
Ok(file)
}
pub(super) fn bounded(path: &Path, code: &'static str) -> Result<Vec<u8>, Error> {
const MAX_BYTES: u64 = 1_048_576;
let mut bytes = Vec::new();
regular(path, code)?
.take(MAX_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| Error::new(code, &error))?;
if bytes.len() as u64 > MAX_BYTES {
return Err(Error::new(
code,
"configuration or manifest exceeds its byte bound",
));
}
Ok(bytes)
}
pub(super) fn entry(path: &Path, code: &'static str) -> Result<Entry, Error> {
let mut file = regular(path, code)?;
let mode = file
.metadata()
.map_err(|error| Error::new(code, &error))?
.permissions()
.mode()
& 0o7777;
let (bytes, sha256) = digest(&mut file, false)?;
Ok(Entry {
bytes,
sha256,
mode,
})
}
fn digest(reader: &mut impl Read, worker: bool) -> Result<(u64, String), Error> {
const MARKER: &[u8] = b"embed-stand-in";
let mut bytes = 0_u64;
let mut hash = Sha256::new();
let mut buffer = vec![0_u8; 65_536 + MARKER.len() - 1].into_boxed_slice();
let mut carried = 0;
loop {
let read = reader
.read(&mut buffer[carried..])
.map_err(|error| Error::new("bundle_read_failed", &error))?;
if read == 0 {
break;
}
bytes = bytes
.checked_add(read as u64)
.ok_or_else(|| Error::new("bundle_size_overflow", "payload byte count overflowed"))?;
let end = carried + read;
hash.update(&buffer[carried..end]);
if worker
&& buffer[..end]
.windows(MARKER.len())
.any(|part| part == MARKER)
{
return Err(Error::new(
"bundle_test_worker_refused",
"a test-only worker cannot be installed",
));
}
carried = if worker { end.min(MARKER.len() - 1) } else { 0 };
buffer.copy_within(end - carried..end, 0);
}
Ok((bytes, format!("{:x}", hash.finalize())))
}
#[cfg(test)]
#[path = "file_tests.rs"]
mod tests;