//! Deterministic and bounded tar/OAR archive handling.
// Public signatures mirror the fixed C# compatibility surface. Several own
// inputs intentionally and RegionSettings intentionally exposes boolean fields.
#![allow(clippy::pedantic)]
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Component, Path, PathBuf};
use std::sync::Mutex;
use flate2::{Compression, GzBuilder, read::GzDecoder};
use libremetaverse_types::compat::ReadWrite;
use libremetaverse_types::{AssetType, Error, UUID};
use crate::assets::TarArchiveReaderTarEntryType;
const BLOCK: usize = 512;
const MAX_ENTRY_BYTES: usize = 64 * 1024 * 1024;
const MAX_TOTAL_BYTES: usize = 1024 * 1024 * 1024;
const MAX_ENTRIES: usize = 100_000;
const MAX_PATH_BYTES: usize = 4096;
const MAX_PATH_DEPTH: usize = 64;
const ARCHIVE_XML: &str = "\n";
const OAR_DIRECTORIES: [&str; 5] = ["assets", "objects", "terrains", "landdata", "settings"];
type AsyncCallback = Box;
type TerrainCallback = Box>, i64, i64) + Send + Sync>;
fn archive_error(context: &'static str) -> Error {
Error::Parse {
position: 0,
context,
}
}
fn io_error(_: std::io::Error) -> Error {
Error::InvalidOperation
}
fn validate_archive_path(path: &str) -> Result<(), Error> {
if path.is_empty()
|| path.len() > MAX_PATH_BYTES
|| path.contains('\0')
|| path.contains('\\')
|| path.starts_with('/')
{
return Err(archive_error("unsafe archive path"));
}
let mut depth = 0usize;
for component in Path::new(path).components() {
match component {
Component::Normal(_) => depth += 1,
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(archive_error("archive path traversal"));
}
}
}
if depth == 0 || depth > MAX_PATH_DEPTH {
return Err(archive_error("archive path depth limit"));
}
Ok(())
}
fn octal(bytes: &[u8]) -> Result {
let text = std::str::from_utf8(bytes)
.map_err(|_| archive_error("tar octal UTF-8"))?
.trim_matches(['\0', ' ']);
if text.is_empty() {
return Ok(0);
}
if !text.bytes().all(|byte| matches!(byte, b'0'..=b'7')) {
return Err(archive_error("tar octal field"));
}
u64::from_str_radix(text, 8).map_err(|_| archive_error("tar octal overflow"))
}
fn padded_octal(value: u64, width: usize) -> Result, Error> {
if width < 2 {
return Err(Error::Argument);
}
let digits = format!("{value:o}");
if digits.len() + 1 > width {
return Err(Error::Argument);
}
let mut result = vec![b'0'; width];
let start = width - 1 - digits.len();
result[start..width - 1].copy_from_slice(digits.as_bytes());
result[width - 1] = 0;
Ok(result)
}
fn split_ustar_path(path: &str) -> Result<(&str, &str), Error> {
validate_archive_path(path)?;
if path.len() <= 100 {
return Ok(("", path));
}
for (index, _) in path.match_indices('/').rev() {
let prefix = &path[..index];
let name = &path[index + 1..];
if prefix.len() <= 155 && name.len() <= 100 && !name.is_empty() {
return Ok((prefix, name));
}
}
Err(archive_error("tar path exceeds ustar limits"))
}
fn write_header(
writer: &mut dyn Write,
path: &str,
size: usize,
entry_type: u8,
) -> Result<(), Error> {
if size > MAX_ENTRY_BYTES {
return Err(archive_error("tar entry size limit"));
}
let (prefix, name) = split_ustar_path(path)?;
let mut header = [0u8; BLOCK];
header[..name.len()].copy_from_slice(name.as_bytes());
header[100..108].copy_from_slice(&padded_octal(
if entry_type == b'5' { 0o755 } else { 0o644 },
8,
)?);
header[108..116].copy_from_slice(&padded_octal(0, 8)?);
header[116..124].copy_from_slice(&padded_octal(0, 8)?);
header[124..136].copy_from_slice(&padded_octal(size as u64, 12)?);
header[136..148].copy_from_slice(&padded_octal(0, 12)?);
header[148..156].fill(b' ');
header[156] = entry_type;
header[257..263].copy_from_slice(b"ustar\0");
header[263..265].copy_from_slice(b"00");
header[345..345 + prefix.len()].copy_from_slice(prefix.as_bytes());
let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
header[148..156].copy_from_slice(&padded_octal(checksum, 8)?);
writer.write_all(&header).map_err(io_error)
}
fn write_entry(
writer: &mut dyn Write,
path: &str,
data: &[u8],
entry_type: u8,
) -> Result<(), Error> {
write_header(writer, path, data.len(), entry_type)?;
writer.write_all(data).map_err(io_error)?;
let padding = (BLOCK - data.len() % BLOCK) % BLOCK;
if padding != 0 {
writer.write_all(&vec![0; padding]).map_err(io_error)?;
}
Ok(())
}
struct ReaderState {
stream: Box,
entries: usize,
total_bytes: usize,
finished: bool,
}
pub struct TarArchiveReader {
state: Mutex,
}
impl std::fmt::Debug for TarArchiveReader {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TarArchiveReader")
.finish_non_exhaustive()
}
}
impl TarArchiveReader {
pub fn new(stream: Box) -> Result {
Ok(Self {
state: Mutex::new(ReaderState {
stream,
entries: 0,
total_bytes: 0,
finished: false,
}),
})
}
pub fn close(&self) -> Result<(), Error> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.finished = true;
Ok(())
}
pub fn convert_octal_bytes_to_decimal(
bytes: Vec,
start_index: i32,
count: i32,
) -> Result {
let start = usize::try_from(start_index).map_err(|_| Error::IndexOutOfRange)?;
let count = usize::try_from(count).map_err(|_| Error::IndexOutOfRange)?;
let end = start.checked_add(count).ok_or(Error::IndexOutOfRange)?;
let value = octal(bytes.get(start..end).ok_or(Error::IndexOutOfRange)?)?;
i32::try_from(value).map_err(|_| Error::IndexOutOfRange)
}
pub fn read_entry(
&self,
file_path: &mut String,
entry_type: &mut TarArchiveReaderTarEntryType,
) -> Result