pub mod archive;
pub mod download;
pub mod manifest;
pub mod signature;
pub mod target;
pub use archive::{
ExtractedFiles, ExtractedPaths, ExtractedRelease, VerifiedArchive, VerifiedMember,
extract_archive, extract_archive_file, inspect_archive, verify_archive_bytes,
verify_archive_file,
};
pub use download::{
DownloadResponse, DownloadedArchive, Downloader, ProgressObserver, ReleaseMetadata,
UreqDownloader, download_archive, fetch_exact_metadata, fetch_latest_metadata,
fetch_latest_metadata_with_keys, fetch_version_metadata_with_keys,
};
pub use manifest::{
Asset, FileDigest, LauncherInfo, Manifest, PayloadInfo, ReleaseAsset, ReleaseManifest,
SelectedAsset,
};
pub use signature::{
SignatureEntry, SignatureEnvelope, TrustedKey, TrustedKeySet, VerifiedSignature, sign_manifest,
sign_manifest_bytes, sign_manifest_multi, verify_manifest, verify_manifest_with_keys,
};
pub use target::{ReleaseTarget, Target};
use sha2::{Digest, Sha256};
use std::fmt;
use std::io::{self, Read};
use std::path::Path;
pub const MAX_ARCHIVE_SIZE: u64 = 256 * 1024 * 1024;
pub const MAX_MEMBER_SIZE: u64 = 256 * 1024 * 1024;
pub const MAX_UNCOMPRESSED_SIZE: u64 = 256 * 1024 * 1024;
pub const MAX_ARCHIVE_MEMBERS: usize = 4096;
pub const MAX_ARCHIVE_MEMBER_NAME: usize = 4096;
pub const MAX_ARCHIVE_METADATA_SIZE: usize = 8 * 1024 * 1024;
pub const MAX_TAR_METADATA_SIZE: u64 = 64 * 1024;
pub const MAX_TAR_DECOMPRESSED_SIZE: u64 = MAX_UNCOMPRESSED_SIZE + 32 * 1024 * 1024;
pub const MAX_METADATA_SIZE: usize = 1024 * 1024;
#[derive(Debug)]
pub enum ReleaseError {
Invalid(String),
TrustAnchorNotConfigured,
Io(io::Error),
Json(serde_json::Error),
Download(String),
Archive(String),
}
pub type Result<T> = std::result::Result<T, ReleaseError>;
pub type Error = ReleaseError;
impl ReleaseError {
pub(crate) fn invalid(message: impl Into<String>) -> Self {
Self::Invalid(message.into())
}
pub(crate) fn archive(message: impl Into<String>) -> Self {
Self::Archive(message.into())
}
pub(crate) fn download(message: impl Into<String>) -> Self {
Self::Download(message.into())
}
}
impl fmt::Display for ReleaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(message) => f.write_str(message),
Self::TrustAnchorNotConfigured => {
f.write_str("trust anchor not configured: no trusted release signing key")
}
Self::Io(error) => write!(f, "release I/O error: {error}"),
Self::Json(error) => write!(f, "release JSON error: {error}"),
Self::Download(message) => write!(f, "release download error: {message}"),
Self::Archive(message) => write!(f, "release archive error: {message}"),
}
}
}
impl std::error::Error for ReleaseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Json(error) => Some(error),
_ => None,
}
}
}
impl From<io::Error> for ReleaseError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<serde_json::Error> for ReleaseError {
fn from(error: serde_json::Error) -> Self {
Self::Json(error)
}
}
pub fn sha256_bytes(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
pub fn sha256_reader<R: Read>(reader: &mut R) -> io::Result<String> {
let mut hasher = Sha256::new();
let mut buffer = [0u8; 64 * 1024];
loop {
let read = reader.read(&mut buffer)?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(hex::encode(hasher.finalize()))
}
pub fn sha256_file(path: &Path) -> io::Result<String> {
let mut file = crate::fs::executable::open_regular_file_secure(path)?;
sha256_reader(&mut file)
}
pub(crate) fn read_limited<R: Read>(
reader: &mut R,
limit: u64,
) -> std::result::Result<Vec<u8>, ReleaseError> {
let mut output = Vec::new();
let mut buffer = [0u8; 64 * 1024];
let mut total = 0u64;
loop {
let read = reader.read(&mut buffer)?;
if read == 0 {
break;
}
total = total
.checked_add(read as u64)
.ok_or_else(|| ReleaseError::invalid("release body size overflow"))?;
if total > limit {
return Err(ReleaseError::invalid(format!(
"release body exceeds {} bytes",
limit
)));
}
output.extend_from_slice(&buffer[..read]);
}
Ok(output)
}
pub(crate) fn verify_bytes(
bytes: &[u8],
expected_size: u64,
expected_sha256: &str,
label: &str,
) -> std::result::Result<(), ReleaseError> {
if bytes.len() as u64 != expected_size {
return Err(ReleaseError::invalid(format!(
"{label} size mismatch: expected {expected_size}, got {}",
bytes.len()
)));
}
let actual = sha256_bytes(bytes);
if actual != expected_sha256 {
return Err(ReleaseError::invalid(format!(
"{label} SHA-256 mismatch: expected {expected_sha256}, got {actual}"
)));
}
Ok(())
}
pub(crate) fn path_is_safe_directory(path: &Path) -> std::result::Result<(), ReleaseError> {
#[cfg(windows)]
crate::fs::security::ensure_private_dir(path).map_err(|error| {
ReleaseError::archive(format!(
"Windows extraction destination is not private {}: {error}",
path.display()
))
})?;
crate::fs::executable::ensure_directory_tree(path).map_err(|error| {
ReleaseError::archive(format!(
"unsafe extraction destination {}: {error}",
path.display()
))
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha256_matches_published_vector() {
assert_eq!(
sha256_bytes(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn reader_hash_is_streaming_equivalent() {
let bytes = (0..=255u8).cycle().take(100_003).collect::<Vec<_>>();
let expected = sha256_bytes(&bytes);
let mut reader = std::io::Cursor::new(bytes);
assert_eq!(sha256_reader(&mut reader).unwrap(), expected);
}
}