Some checks failed
Native code generation / deterministic (push) Failing after 1m48s
Imaging and meshing gate / native (push) Successful in 5m26s
JPEG 2000 feature / linux (push) Successful in 2m51s
Native Rust workspace compile / compile (push) Successful in 5m28s
Skia feature / linux (push) Successful in 31m53s
1262 lines
45 KiB
Rust
1262 lines
45 KiB
Rust
//! 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 = "<?xml version=\"1.0\" encoding=\"utf-16\"?>\n<archive major_version=\"0\" minor_version=\"1\" />";
|
|
const OAR_DIRECTORIES: [&str; 5] = ["assets", "objects", "terrains", "landdata", "settings"];
|
|
|
|
type AsyncCallback = Box<dyn Fn(&dyn std::any::Any) + Send + Sync>;
|
|
type TerrainCallback = Box<dyn Fn(Vec<Vec<f32>>, 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<u64, Error> {
|
|
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<Vec<u8>, 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<dyn ReadWrite + Send>,
|
|
entries: usize,
|
|
total_bytes: usize,
|
|
finished: bool,
|
|
}
|
|
|
|
pub struct TarArchiveReader {
|
|
state: Mutex<ReaderState>,
|
|
}
|
|
|
|
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<dyn ReadWrite + Send>) -> Result<Self, Error> {
|
|
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<u8>,
|
|
start_index: i32,
|
|
count: i32,
|
|
) -> Result<i32, Error> {
|
|
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<Option<Vec<u8>>, Error> {
|
|
let mut state = self
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if state.finished {
|
|
return Ok(None);
|
|
}
|
|
let mut header = [0u8; BLOCK];
|
|
state.stream.read_exact(&mut header).map_err(io_error)?;
|
|
if header.iter().all(|byte| *byte == 0) {
|
|
state.finished = true;
|
|
return Ok(None);
|
|
}
|
|
let recorded = octal(&header[148..156])?;
|
|
let mut checksum_header = header;
|
|
checksum_header[148..156].fill(b' ');
|
|
let computed: u64 = checksum_header.iter().map(|byte| u64::from(*byte)).sum();
|
|
if recorded != computed {
|
|
return Err(archive_error("tar checksum"));
|
|
}
|
|
let name = nul_string(&header[..100])?;
|
|
let prefix = nul_string(&header[345..500])?;
|
|
let path = if prefix.is_empty() {
|
|
name
|
|
} else {
|
|
format!("{prefix}/{name}")
|
|
};
|
|
validate_archive_path(&path)?;
|
|
let size = usize::try_from(octal(&header[124..136])?)
|
|
.map_err(|_| archive_error("tar size overflow"))?;
|
|
if size > MAX_ENTRY_BYTES {
|
|
return Err(archive_error("tar entry size limit"));
|
|
}
|
|
state.entries = state
|
|
.entries
|
|
.checked_add(1)
|
|
.ok_or_else(|| archive_error("tar entry count overflow"))?;
|
|
state.total_bytes = state
|
|
.total_bytes
|
|
.checked_add(size)
|
|
.ok_or_else(|| archive_error("tar total size overflow"))?;
|
|
if state.entries > MAX_ENTRIES || state.total_bytes > MAX_TOTAL_BYTES {
|
|
return Err(archive_error("tar archive limits"));
|
|
}
|
|
let kind = match header[156] {
|
|
0 | b'0' => TarArchiveReaderTarEntryType::TYPE_NORMAL_FILE,
|
|
b'1' => TarArchiveReaderTarEntryType::TYPE_HARD_LINK,
|
|
b'2' => TarArchiveReaderTarEntryType::TYPE_SYMBOLIC_LINK,
|
|
b'3' => TarArchiveReaderTarEntryType::TYPE_CHAR_SPECIAL,
|
|
b'4' => TarArchiveReaderTarEntryType::TYPE_BLOCK_SPECIAL,
|
|
b'5' => TarArchiveReaderTarEntryType::TYPE_DIRECTORY,
|
|
b'6' => TarArchiveReaderTarEntryType::TYPE_FIFO,
|
|
b'7' => TarArchiveReaderTarEntryType::TYPE_CONTIGUOUS_FILE,
|
|
_ => TarArchiveReaderTarEntryType::TYPE_UNKNOWN,
|
|
};
|
|
if matches!(
|
|
kind,
|
|
TarArchiveReaderTarEntryType::TYPE_SYMBOLIC_LINK
|
|
| TarArchiveReaderTarEntryType::TYPE_HARD_LINK
|
|
) {
|
|
return Err(archive_error("tar links are not permitted"));
|
|
}
|
|
let mut data = vec![0u8; size];
|
|
state.stream.read_exact(&mut data).map_err(io_error)?;
|
|
let padding = (BLOCK - size % BLOCK) % BLOCK;
|
|
if padding != 0 {
|
|
state
|
|
.stream
|
|
.seek(SeekFrom::Current(padding as i64))
|
|
.map_err(io_error)?;
|
|
}
|
|
*file_path = path;
|
|
*entry_type = kind;
|
|
Ok(Some(data))
|
|
}
|
|
}
|
|
|
|
fn nul_string(bytes: &[u8]) -> Result<String, Error> {
|
|
let end = bytes
|
|
.iter()
|
|
.position(|byte| *byte == 0)
|
|
.unwrap_or(bytes.len());
|
|
std::str::from_utf8(&bytes[..end])
|
|
.map(str::to_owned)
|
|
.map_err(|_| archive_error("tar path UTF-8"))
|
|
}
|
|
|
|
struct WriterState {
|
|
stream: Box<dyn ReadWrite + Send>,
|
|
entries: usize,
|
|
total_bytes: usize,
|
|
closed: bool,
|
|
}
|
|
|
|
pub struct TarArchiveWriter {
|
|
state: Mutex<WriterState>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct OarFileAssetLoadedCallback(
|
|
std::sync::Arc<dyn Fn(crate::assets::Asset, i64, i64) + Send + Sync>,
|
|
);
|
|
impl OarFileAssetLoadedCallback {
|
|
pub fn from_callback(
|
|
callback: impl Fn(crate::assets::Asset, i64, i64) + Send + Sync + 'static,
|
|
) -> Self {
|
|
Self(std::sync::Arc::new(callback))
|
|
}
|
|
pub fn new(
|
|
_object: libremetaverse_types::compat::Object,
|
|
_method: isize,
|
|
) -> Result<Self, Error> {
|
|
Err(Error::Argument)
|
|
}
|
|
pub fn invoke(
|
|
&self,
|
|
asset: crate::assets::Asset,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
) -> Result<(), Error> {
|
|
(self.0)(asset, bytes_read, total_bytes);
|
|
Ok(())
|
|
}
|
|
pub fn begin_invoke(
|
|
&self,
|
|
asset: crate::assets::Asset,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
callback: AsyncCallback,
|
|
_object: libremetaverse_types::compat::Object,
|
|
) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
|
|
self.invoke(asset, bytes_read, total_bytes)?;
|
|
callback(&());
|
|
Ok(Box::new(()))
|
|
}
|
|
pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct OarFileTerrainLoadedCallback(std::sync::Arc<dyn Fn(Vec<f32>, i64, i64) + Send + Sync>);
|
|
impl OarFileTerrainLoadedCallback {
|
|
pub fn from_callback(callback: impl Fn(Vec<f32>, i64, i64) + Send + Sync + 'static) -> Self {
|
|
Self(std::sync::Arc::new(callback))
|
|
}
|
|
pub fn new(
|
|
_object: libremetaverse_types::compat::Object,
|
|
_method: isize,
|
|
) -> Result<Self, Error> {
|
|
Err(Error::Argument)
|
|
}
|
|
pub fn invoke(
|
|
&self,
|
|
terrain: Vec<f32>,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
) -> Result<(), Error> {
|
|
(self.0)(terrain, bytes_read, total_bytes);
|
|
Ok(())
|
|
}
|
|
pub fn begin_invoke(
|
|
&self,
|
|
terrain: Vec<f32>,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
callback: AsyncCallback,
|
|
_object: libremetaverse_types::compat::Object,
|
|
) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
|
|
self.invoke(terrain, bytes_read, total_bytes)?;
|
|
callback(&());
|
|
Ok(Box::new(()))
|
|
}
|
|
pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct OarFileSettingsLoadedCallback(
|
|
std::sync::Arc<dyn Fn(String, RegionSettings) + Send + Sync>,
|
|
);
|
|
impl OarFileSettingsLoadedCallback {
|
|
pub fn from_callback(
|
|
callback: impl Fn(String, RegionSettings) + Send + Sync + 'static,
|
|
) -> Self {
|
|
Self(std::sync::Arc::new(callback))
|
|
}
|
|
pub fn new(
|
|
_object: libremetaverse_types::compat::Object,
|
|
_method: isize,
|
|
) -> Result<Self, Error> {
|
|
Err(Error::Argument)
|
|
}
|
|
pub fn invoke(&self, region_name: String, settings: RegionSettings) -> Result<(), Error> {
|
|
(self.0)(region_name, settings);
|
|
Ok(())
|
|
}
|
|
pub fn begin_invoke(
|
|
&self,
|
|
region_name: String,
|
|
settings: RegionSettings,
|
|
callback: AsyncCallback,
|
|
_object: libremetaverse_types::compat::Object,
|
|
) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
|
|
self.invoke(region_name, settings)?;
|
|
callback(&());
|
|
Ok(Box::new(()))
|
|
}
|
|
pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct OarFileSceneObjectLoadedCallback(
|
|
std::sync::Arc<dyn Fn(crate::assets::AssetPrim, i64, i64) + Send + Sync>,
|
|
);
|
|
impl OarFileSceneObjectLoadedCallback {
|
|
pub fn from_callback(
|
|
callback: impl Fn(crate::assets::AssetPrim, i64, i64) + Send + Sync + 'static,
|
|
) -> Self {
|
|
Self(std::sync::Arc::new(callback))
|
|
}
|
|
pub fn new(
|
|
_object: libremetaverse_types::compat::Object,
|
|
_method: isize,
|
|
) -> Result<Self, Error> {
|
|
Err(Error::Argument)
|
|
}
|
|
pub fn invoke(
|
|
&self,
|
|
linkset: crate::assets::AssetPrim,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
) -> Result<(), Error> {
|
|
(self.0)(linkset, bytes_read, total_bytes);
|
|
Ok(())
|
|
}
|
|
pub fn begin_invoke(
|
|
&self,
|
|
linkset: crate::assets::AssetPrim,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
callback: AsyncCallback,
|
|
_object: libremetaverse_types::compat::Object,
|
|
) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
|
|
self.invoke(linkset, bytes_read, total_bytes)?;
|
|
callback(&());
|
|
Ok(Box::new(()))
|
|
}
|
|
pub fn end_invoke(&self, _result: Box<dyn std::any::Any + Send + Sync>) -> Result<(), Error> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct RegionSettings {
|
|
pub agent_limit: i32,
|
|
pub allow_damage: bool,
|
|
pub allow_land_join_divide: bool,
|
|
pub allow_land_resell: bool,
|
|
pub block_fly: bool,
|
|
pub block_land_show_in_search: bool,
|
|
pub block_terraform: bool,
|
|
pub disable_collisions: bool,
|
|
pub disable_physics: bool,
|
|
pub disable_scripts: bool,
|
|
pub fixed_sun: bool,
|
|
pub maturity_rating: i32,
|
|
pub object_bonus: f32,
|
|
pub restrict_pushing: bool,
|
|
pub terrain_detail0: UUID,
|
|
pub terrain_detail1: UUID,
|
|
pub terrain_detail2: UUID,
|
|
pub terrain_detail3: UUID,
|
|
pub terrain_height_range00: f32,
|
|
pub terrain_height_range01: f32,
|
|
pub terrain_height_range10: f32,
|
|
pub terrain_height_range11: f32,
|
|
pub terrain_lower_limit: f32,
|
|
pub terrain_raise_limit: f32,
|
|
pub terrain_start_height00: f32,
|
|
pub terrain_start_height01: f32,
|
|
pub terrain_start_height10: f32,
|
|
pub terrain_start_height11: f32,
|
|
pub use_estate_sun: bool,
|
|
pub water_height: f32,
|
|
}
|
|
impl RegionSettings {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self::default())
|
|
}
|
|
pub fn from_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
|
|
stream.seek(SeekFrom::Start(0)).map_err(io_error)?;
|
|
let mut bytes = Vec::new();
|
|
stream
|
|
.take((MAX_ENTRY_BYTES + 1) as u64)
|
|
.read_to_end(&mut bytes)
|
|
.map_err(io_error)?;
|
|
if bytes.len() > MAX_ENTRY_BYTES {
|
|
return Err(Error::Argument);
|
|
}
|
|
let text =
|
|
std::str::from_utf8(&bytes).map_err(|_| archive_error("region settings UTF-8"))?;
|
|
let document =
|
|
roxmltree::Document::parse(text).map_err(|_| archive_error("region settings XML"))?;
|
|
if !document.root_element().has_tag_name("RegionSettings") {
|
|
return Err(archive_error("region settings root"));
|
|
}
|
|
let mut settings = Self::default();
|
|
for node in document.descendants().filter(|node| node.is_element()) {
|
|
let text = node.text().unwrap_or_default().trim();
|
|
match node.tag_name().name() {
|
|
"AllowDamage" => settings.allow_damage = parse_bool(text)?,
|
|
"AllowLandResell" => settings.allow_land_resell = parse_bool(text)?,
|
|
"AllowLandJoinDivide" => settings.allow_land_join_divide = parse_bool(text)?,
|
|
"BlockFly" => settings.block_fly = parse_bool(text)?,
|
|
"BlockLandShowInSearch" => settings.block_land_show_in_search = parse_bool(text)?,
|
|
"BlockTerraform" => settings.block_terraform = parse_bool(text)?,
|
|
"DisableCollisions" => settings.disable_collisions = parse_bool(text)?,
|
|
"DisablePhysics" => settings.disable_physics = parse_bool(text)?,
|
|
"DisableScripts" => settings.disable_scripts = parse_bool(text)?,
|
|
"FixedSun" => settings.fixed_sun = parse_bool(text)?,
|
|
"RestrictPushing" => settings.restrict_pushing = parse_bool(text)?,
|
|
"UseEstateSun" => settings.use_estate_sun = parse_bool(text)?,
|
|
"MaturityRating" => settings.maturity_rating = parse_i32(text)?,
|
|
"AgentLimit" => settings.agent_limit = parse_i32(text)?,
|
|
"ObjectBonus" => settings.object_bonus = parse_f32(text)?,
|
|
"WaterHeight" => settings.water_height = parse_f32(text)?,
|
|
"TerrainRaiseLimit" => settings.terrain_raise_limit = parse_f32(text)?,
|
|
"TerrainLowerLimit" => settings.terrain_lower_limit = parse_f32(text)?,
|
|
"Texture1" => settings.terrain_detail0 = UUID::new_with_string(text.to_owned())?,
|
|
"Texture2" => settings.terrain_detail1 = UUID::new_with_string(text.to_owned())?,
|
|
"Texture3" => settings.terrain_detail2 = UUID::new_with_string(text.to_owned())?,
|
|
"Texture4" => settings.terrain_detail3 = UUID::new_with_string(text.to_owned())?,
|
|
"ElevationLowSW" => settings.terrain_start_height00 = parse_f32(text)?,
|
|
"ElevationLowNW" => settings.terrain_start_height01 = parse_f32(text)?,
|
|
"ElevationLowSE" => settings.terrain_start_height10 = parse_f32(text)?,
|
|
"ElevationLowNE" => settings.terrain_start_height11 = parse_f32(text)?,
|
|
"ElevationHighSW" => settings.terrain_height_range00 = parse_f32(text)?,
|
|
"ElevationHighNW" => settings.terrain_height_range01 = parse_f32(text)?,
|
|
"ElevationHighSE" => settings.terrain_height_range10 = parse_f32(text)?,
|
|
"ElevationHighNE" => settings.terrain_height_range11 = parse_f32(text)?,
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(settings)
|
|
}
|
|
pub fn to_xml(&self, filename: String) -> Result<(), Error> {
|
|
fs::write(filename, self.xml()).map_err(io_error)
|
|
}
|
|
fn xml(&self) -> String {
|
|
format!(
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RegionSettings><General><AllowDamage>{}</AllowDamage><AllowLandResell>{}</AllowLandResell><AllowLandJoinDivide>{}</AllowLandJoinDivide><BlockFly>{}</BlockFly><BlockLandShowInSearch>{}</BlockLandShowInSearch><BlockTerraform>{}</BlockTerraform><DisableCollisions>{}</DisableCollisions><DisablePhysics>{}</DisablePhysics><DisableScripts>{}</DisableScripts><MaturityRating>{}</MaturityRating><RestrictPushing>{}</RestrictPushing><AgentLimit>{}</AgentLimit><ObjectBonus>{}</ObjectBonus></General><GroundTextures><Texture1>{}</Texture1><Texture2>{}</Texture2><Texture3>{}</Texture3><Texture4>{}</Texture4><ElevationLowSW>{}</ElevationLowSW><ElevationLowNW>{}</ElevationLowNW><ElevationLowSE>{}</ElevationLowSE><ElevationLowNE>{}</ElevationLowNE><ElevationHighSW>{}</ElevationHighSW><ElevationHighNW>{}</ElevationHighNW><ElevationHighSE>{}</ElevationHighSE><ElevationHighNE>{}</ElevationHighNE></GroundTextures><Terrain><WaterHeight>{}</WaterHeight><TerrainRaiseLimit>{}</TerrainRaiseLimit><TerrainLowerLimit>{}</TerrainLowerLimit><UseEstateSun>{}</UseEstateSun><FixedSun>{}</FixedSun></Terrain></RegionSettings>\n",
|
|
self.allow_damage,
|
|
self.allow_land_resell,
|
|
self.allow_land_join_divide,
|
|
self.block_fly,
|
|
self.block_land_show_in_search,
|
|
self.block_terraform,
|
|
self.disable_collisions,
|
|
self.disable_physics,
|
|
self.disable_scripts,
|
|
self.maturity_rating,
|
|
self.restrict_pushing,
|
|
self.agent_limit,
|
|
self.object_bonus,
|
|
self.terrain_detail0.to_string(),
|
|
self.terrain_detail1.to_string(),
|
|
self.terrain_detail2.to_string(),
|
|
self.terrain_detail3.to_string(),
|
|
self.terrain_start_height00,
|
|
self.terrain_start_height01,
|
|
self.terrain_start_height10,
|
|
self.terrain_start_height11,
|
|
self.terrain_height_range00,
|
|
self.terrain_height_range01,
|
|
self.terrain_height_range10,
|
|
self.terrain_height_range11,
|
|
self.water_height,
|
|
self.terrain_raise_limit,
|
|
self.terrain_lower_limit,
|
|
self.use_estate_sun,
|
|
self.fixed_sun
|
|
)
|
|
}
|
|
}
|
|
impl Default for RegionSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
agent_limit: 0,
|
|
allow_damage: false,
|
|
allow_land_join_divide: false,
|
|
allow_land_resell: false,
|
|
block_fly: false,
|
|
block_land_show_in_search: false,
|
|
block_terraform: false,
|
|
disable_collisions: false,
|
|
disable_physics: false,
|
|
disable_scripts: false,
|
|
fixed_sun: false,
|
|
maturity_rating: 0,
|
|
object_bonus: 0.0,
|
|
restrict_pushing: false,
|
|
terrain_detail0: UUID::zero(),
|
|
terrain_detail1: UUID::zero(),
|
|
terrain_detail2: UUID::zero(),
|
|
terrain_detail3: UUID::zero(),
|
|
terrain_height_range00: 0.0,
|
|
terrain_height_range01: 0.0,
|
|
terrain_height_range10: 0.0,
|
|
terrain_height_range11: 0.0,
|
|
terrain_lower_limit: 0.0,
|
|
terrain_raise_limit: 0.0,
|
|
terrain_start_height00: 0.0,
|
|
terrain_start_height01: 0.0,
|
|
terrain_start_height10: 0.0,
|
|
terrain_start_height11: 0.0,
|
|
use_estate_sun: false,
|
|
water_height: 0.0,
|
|
}
|
|
}
|
|
}
|
|
fn parse_bool(value: &str) -> Result<bool, Error> {
|
|
match value.to_ascii_lowercase().as_str() {
|
|
"true" => Ok(true),
|
|
"false" => Ok(false),
|
|
_ => Err(archive_error("region settings boolean")),
|
|
}
|
|
}
|
|
fn parse_i32(value: &str) -> Result<i32, Error> {
|
|
value
|
|
.parse()
|
|
.map_err(|_| archive_error("region settings integer"))
|
|
}
|
|
fn parse_f32(value: &str) -> Result<f32, Error> {
|
|
value
|
|
.parse::<f32>()
|
|
.ok()
|
|
.filter(|value| value.is_finite())
|
|
.ok_or_else(|| archive_error("region settings number"))
|
|
}
|
|
impl std::fmt::Debug for TarArchiveWriter {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
formatter
|
|
.debug_struct("TarArchiveWriter")
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
impl TarArchiveWriter {
|
|
pub fn new(stream: Box<dyn ReadWrite + Send>) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
state: Mutex::new(WriterState {
|
|
stream,
|
|
entries: 0,
|
|
total_bytes: 0,
|
|
closed: false,
|
|
}),
|
|
})
|
|
}
|
|
pub fn convert_decimal_to_padded_octal_bytes(
|
|
value: i32,
|
|
padding: i32,
|
|
) -> Result<Vec<u8>, Error> {
|
|
padded_octal(
|
|
u64::try_from(value).map_err(|_| Error::Argument)?,
|
|
usize::try_from(padding).map_err(|_| Error::Argument)?,
|
|
)
|
|
}
|
|
pub fn write_dir(&self, dir_name: String) -> Result<(), Error> {
|
|
self.write(&dir_name, &[], b'5')
|
|
}
|
|
pub fn write_file_with_string_bytes(
|
|
&self,
|
|
file_path: String,
|
|
data: Vec<u8>,
|
|
) -> Result<(), Error> {
|
|
self.write(&file_path, &data, b'0')
|
|
}
|
|
pub fn write_file_with_string_string(
|
|
&self,
|
|
file_path: String,
|
|
data: String,
|
|
) -> Result<(), Error> {
|
|
self.write(&file_path, data.as_bytes(), b'0')
|
|
}
|
|
fn write(&self, path: &str, data: &[u8], kind: u8) -> Result<(), Error> {
|
|
let mut state = self
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if state.closed {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
state.entries = state
|
|
.entries
|
|
.checked_add(1)
|
|
.ok_or(Error::InvalidOperation)?;
|
|
state.total_bytes = state
|
|
.total_bytes
|
|
.checked_add(data.len())
|
|
.ok_or(Error::InvalidOperation)?;
|
|
if state.entries > MAX_ENTRIES || state.total_bytes > MAX_TOTAL_BYTES {
|
|
return Err(archive_error("tar archive limits"));
|
|
}
|
|
write_entry(&mut state.stream, path, data, kind)
|
|
}
|
|
pub fn close(&self) -> Result<(), Error> {
|
|
let mut state = self
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if !state.closed {
|
|
state
|
|
.stream
|
|
.write_all(&[0u8; BLOCK * 2])
|
|
.map_err(io_error)?;
|
|
state.stream.flush().map_err(io_error)?;
|
|
state.closed = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(crate) fn clear_asset_folder(assets_path: String) -> Result<(), Error> {
|
|
let root = PathBuf::from(assets_path);
|
|
if !root.is_dir() {
|
|
return fs::create_dir_all(root).map_err(io_error);
|
|
}
|
|
for entry in fs::read_dir(&root).map_err(io_error)? {
|
|
let entry = entry.map_err(io_error)?;
|
|
let metadata = fs::symlink_metadata(entry.path()).map_err(io_error)?;
|
|
if metadata.file_type().is_symlink() || metadata.is_file() {
|
|
fs::remove_file(entry.path()).map_err(io_error)?;
|
|
} else if metadata.is_dir() {
|
|
fs::remove_dir_all(entry.path()).map_err(io_error)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn asset_extension(asset_type: AssetType) -> Option<&'static str> {
|
|
match asset_type {
|
|
AssetType::Animation => Some("_animation.bvh"),
|
|
AssetType::Bodypart => Some("_bodypart.txt"),
|
|
AssetType::CallingCard => Some("_callingcard.txt"),
|
|
AssetType::Clothing => Some("_clothing.txt"),
|
|
AssetType::Folder => Some("_folder.txt"),
|
|
AssetType::Gesture => Some("_gesture.txt"),
|
|
AssetType::ImageJPEG => Some("_image.jpg"),
|
|
AssetType::ImageTGA => Some("_image.tga"),
|
|
AssetType::Landmark => Some("_landmark.txt"),
|
|
AssetType::LSLBytecode => Some("_bytecode.lso"),
|
|
AssetType::LSLText => Some("_script.lsl"),
|
|
AssetType::Notecard => Some("_notecard.txt"),
|
|
AssetType::Object => Some("_object.xml"),
|
|
AssetType::Simstate => Some("_simstate.bin"),
|
|
AssetType::Sound => Some("_sound.ogg"),
|
|
AssetType::SoundWAV => Some("_sound.wav"),
|
|
AssetType::Texture => Some("_texture.jp2"),
|
|
AssetType::TextureTGA => Some("_texture.tga"),
|
|
AssetType::Mesh => Some("_mesh.llmesh"),
|
|
AssetType::Material => Some("_material.llsd"),
|
|
AssetType::Settings => Some("_settings.llsd"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn asset_type_to_extension()
|
|
-> libremetaverse_types::compat::FrozenDictionary<AssetType, String> {
|
|
let types = [
|
|
AssetType::Animation,
|
|
AssetType::Bodypart,
|
|
AssetType::CallingCard,
|
|
AssetType::Clothing,
|
|
AssetType::Folder,
|
|
AssetType::Gesture,
|
|
AssetType::ImageJPEG,
|
|
AssetType::ImageTGA,
|
|
AssetType::Landmark,
|
|
AssetType::LSLBytecode,
|
|
AssetType::LSLText,
|
|
AssetType::Notecard,
|
|
AssetType::Object,
|
|
AssetType::Simstate,
|
|
AssetType::Sound,
|
|
AssetType::SoundWAV,
|
|
AssetType::Texture,
|
|
AssetType::TextureTGA,
|
|
AssetType::Mesh,
|
|
AssetType::Material,
|
|
AssetType::Settings,
|
|
];
|
|
libremetaverse_types::compat::FrozenDictionary(
|
|
types
|
|
.into_iter()
|
|
.filter_map(|kind| asset_extension(kind).map(|suffix| (kind, suffix.to_owned())))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn extension_to_asset_type()
|
|
-> libremetaverse_types::compat::FrozenDictionary<String, AssetType> {
|
|
libremetaverse_types::compat::FrozenDictionary(
|
|
asset_type_to_extension()
|
|
.0
|
|
.into_iter()
|
|
.map(|(kind, suffix)| (suffix, kind))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn prepare_output_directory(path: &str) -> Result<PathBuf, Error> {
|
|
let root = PathBuf::from(path);
|
|
if root.exists() {
|
|
let metadata = fs::symlink_metadata(&root).map_err(io_error)?;
|
|
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
|
return Err(archive_error("archive output root"));
|
|
}
|
|
} else {
|
|
fs::create_dir_all(&root).map_err(io_error)?;
|
|
}
|
|
Ok(root)
|
|
}
|
|
|
|
fn write_asset(root: &Path, id: UUID, extension: &str, data: &[u8]) -> Result<(), Error> {
|
|
if data.len() > MAX_ENTRY_BYTES {
|
|
return Err(archive_error("asset size limit"));
|
|
}
|
|
let name = format!("{}{extension}", id);
|
|
validate_archive_path(&name)?;
|
|
fs::write(root.join(name), data).map_err(io_error)
|
|
}
|
|
|
|
pub(crate) async fn save_assets(
|
|
asset_manager: crate::AssetManager,
|
|
asset_type: AssetType,
|
|
mut assets: Vec<UUID>,
|
|
assets_path: String,
|
|
) -> Result<(), Error> {
|
|
if assets.len() > MAX_ENTRIES {
|
|
return Err(archive_error("asset count limit"));
|
|
}
|
|
let extension = asset_extension(asset_type).ok_or(Error::Argument)?;
|
|
let root = prepare_output_directory(&assets_path)?;
|
|
assets.sort_by_key(UUID::to_string);
|
|
assets.dedup();
|
|
let mut total = 0usize;
|
|
for id in assets {
|
|
if id == UUID::zero() {
|
|
continue;
|
|
}
|
|
let bytes = if asset_type == AssetType::Texture {
|
|
asset_manager
|
|
.request_image(id, None, None)
|
|
.await?
|
|
.map(|asset| asset.native_bytes())
|
|
} else {
|
|
asset_manager
|
|
.request_asset_with_uuid_asset_type_boolean_cancellation_token(
|
|
id, asset_type, false, None,
|
|
)
|
|
.await?
|
|
.map(|asset| asset.asset_data)
|
|
};
|
|
if let Some(bytes) = bytes {
|
|
total = total
|
|
.checked_add(bytes.len())
|
|
.ok_or_else(|| archive_error("asset total size overflow"))?;
|
|
if total > MAX_TOTAL_BYTES {
|
|
return Err(archive_error("asset total size limit"));
|
|
}
|
|
write_asset(&root, id, extension, &bytes)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn save_sim_asset(
|
|
asset_manager: crate::AssetManager,
|
|
asset_type: AssetType,
|
|
asset_id: UUID,
|
|
item_id: UUID,
|
|
prim_id: UUID,
|
|
assets_path: String,
|
|
) -> Result<(), Error> {
|
|
let extension = asset_extension(asset_type).ok_or(Error::Argument)?;
|
|
let root = prepare_output_directory(&assets_path)?;
|
|
let asset = asset_manager
|
|
.request_asset_with_uuid_uuid_uuid_asset_type_boolean_source_type_uuid_cancellation_token(
|
|
asset_id,
|
|
item_id,
|
|
prim_id,
|
|
asset_type,
|
|
false,
|
|
crate::SourceType::SimInventoryItem,
|
|
UUID::random()?,
|
|
None,
|
|
)
|
|
.await?;
|
|
if let Some(asset) = asset {
|
|
write_asset(&root, asset_id, extension, &asset.asset_data)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn package_archive(directory_name: String, filename: String) -> Result<(), Error> {
|
|
let root = fs::canonicalize(&directory_name).map_err(io_error)?;
|
|
if !root.is_dir() {
|
|
return Err(Error::Argument);
|
|
}
|
|
let output = File::create(filename).map_err(io_error)?;
|
|
let mut gzip = GzBuilder::new()
|
|
.mtime(0)
|
|
.write(output, Compression::default());
|
|
write_entry(&mut gzip, "archive.xml", ARCHIVE_XML.as_bytes(), b'0')?;
|
|
let mut entries = Vec::new();
|
|
for directory in OAR_DIRECTORIES {
|
|
let path = root.join(directory);
|
|
if path.is_dir() {
|
|
collect_files(&root, &path, 1, &mut entries)?;
|
|
}
|
|
}
|
|
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
|
let mut total = ARCHIVE_XML.len();
|
|
if entries.len() + 1 > MAX_ENTRIES {
|
|
return Err(archive_error("OAR entry count limit"));
|
|
}
|
|
for (archive_path, disk_path) in entries {
|
|
let data = fs::read(disk_path).map_err(io_error)?;
|
|
total = total
|
|
.checked_add(data.len())
|
|
.ok_or_else(|| archive_error("OAR total size overflow"))?;
|
|
if data.len() > MAX_ENTRY_BYTES || total > MAX_TOTAL_BYTES {
|
|
return Err(archive_error("OAR size limit"));
|
|
}
|
|
write_entry(&mut gzip, &archive_path, &data, b'0')?;
|
|
}
|
|
gzip.write_all(&[0u8; BLOCK * 2]).map_err(io_error)?;
|
|
gzip.finish().map_err(io_error)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_files(
|
|
root: &Path,
|
|
directory: &Path,
|
|
depth: usize,
|
|
result: &mut Vec<(String, PathBuf)>,
|
|
) -> Result<(), Error> {
|
|
if depth > MAX_PATH_DEPTH {
|
|
return Err(archive_error("OAR directory depth limit"));
|
|
}
|
|
let mut children = fs::read_dir(directory)
|
|
.map_err(io_error)?
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.map_err(io_error)?;
|
|
children.sort_by_key(std::fs::DirEntry::file_name);
|
|
for child in children {
|
|
let metadata = fs::symlink_metadata(child.path()).map_err(io_error)?;
|
|
if metadata.file_type().is_symlink() {
|
|
return Err(archive_error("OAR symlink rejected"));
|
|
}
|
|
if metadata.is_dir() {
|
|
collect_files(root, &child.path(), depth + 1, result)?;
|
|
} else if metadata.is_file() {
|
|
let relative = child
|
|
.path()
|
|
.strip_prefix(root)
|
|
.map_err(|_| archive_error("OAR extraction root"))?
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
validate_archive_path(&relative)?;
|
|
result.push((relative, child.path()));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn unpackage_archive(
|
|
filename: String,
|
|
asset_callback: OarFileAssetLoadedCallback,
|
|
terrain_callback: OarFileTerrainLoadedCallback,
|
|
_object_callback: OarFileSceneObjectLoadedCallback,
|
|
settings_callback: OarFileSettingsLoadedCallback,
|
|
) -> Result<(), Error> {
|
|
let file = File::open(filename).map_err(io_error)?;
|
|
let total = i64::try_from(file.metadata().map_err(io_error)?.len())
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
let decoder = GzDecoder::new(file);
|
|
let mut archive = tar::Archive::new(decoder);
|
|
let mut count = 0usize;
|
|
let mut expanded = 0usize;
|
|
for item in archive.entries().map_err(io_error)? {
|
|
let entry = item.map_err(io_error)?;
|
|
count += 1;
|
|
if count > MAX_ENTRIES {
|
|
return Err(archive_error("OAR entry count limit"));
|
|
}
|
|
let path = entry
|
|
.path()
|
|
.map_err(io_error)?
|
|
.to_string_lossy()
|
|
.replace('\\', "/");
|
|
validate_archive_path(&path)?;
|
|
let size = usize::try_from(entry.size()).map_err(|_| archive_error("OAR entry size"))?;
|
|
expanded = expanded
|
|
.checked_add(size)
|
|
.ok_or_else(|| archive_error("OAR expanded size overflow"))?;
|
|
if size > MAX_ENTRY_BYTES || expanded > MAX_TOTAL_BYTES {
|
|
return Err(archive_error("OAR expanded size limit"));
|
|
}
|
|
if entry.header().entry_type().is_symlink() || entry.header().entry_type().is_hard_link() {
|
|
return Err(archive_error("OAR links are not permitted"));
|
|
}
|
|
if !entry.header().entry_type().is_file() {
|
|
continue;
|
|
}
|
|
let mut data = Vec::with_capacity(size);
|
|
entry
|
|
.take((MAX_ENTRY_BYTES + 1) as u64)
|
|
.read_to_end(&mut data)
|
|
.map_err(io_error)?;
|
|
if data.len() != size {
|
|
return Err(archive_error("OAR entry length"));
|
|
}
|
|
if path.starts_with("terrains/") {
|
|
let flattened = decode_terrain(&path, &data)?.unwrap_or_default();
|
|
if !flattened.is_empty() {
|
|
terrain_callback.invoke(flattened, expanded as i64, total)?;
|
|
}
|
|
} else if path.starts_with("assets/") {
|
|
if let Some(asset) = load_generic_asset(&path, data)? {
|
|
asset_callback.invoke(asset, expanded as i64, total)?;
|
|
}
|
|
} else if path.starts_with("settings/") && path.ends_with(".xml") {
|
|
let settings = RegionSettings::from_stream(Box::new(std::io::Cursor::new(data)))?;
|
|
let name = Path::new(&path)
|
|
.file_stem()
|
|
.and_then(|value| value.to_str())
|
|
.ok_or_else(|| archive_error("OAR settings name"))?
|
|
.to_owned();
|
|
settings_callback.invoke(name, settings)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn load_generic_asset(path: &str, data: Vec<u8>) -> Result<Option<crate::assets::Asset>, Error> {
|
|
let filename = path
|
|
.rsplit('/')
|
|
.next()
|
|
.ok_or_else(|| archive_error("OAR asset name"))?;
|
|
let Some((suffix, asset_type)) = extension_asset_type(filename) else {
|
|
return Ok(None);
|
|
};
|
|
let Some(id_text) = filename.strip_suffix(suffix) else {
|
|
return Ok(None);
|
|
};
|
|
let id = UUID::new_with_string(id_text.to_owned())?;
|
|
crate::asset_models::Asset::native_new(asset_type, id, data).map(Some)
|
|
}
|
|
|
|
fn extension_asset_type(filename: &str) -> Option<(&'static str, AssetType)> {
|
|
asset_type_to_extension()
|
|
.0
|
|
.into_iter()
|
|
.find_map(|(kind, _)| {
|
|
asset_extension(kind)
|
|
.filter(|suffix| filename.ends_with(suffix))
|
|
.map(|suffix| (suffix, kind))
|
|
})
|
|
}
|
|
|
|
fn decode_terrain(path: &str, data: &[u8]) -> Result<Option<Vec<f32>>, Error> {
|
|
if !path.ends_with(".r32") && !path.ends_with(".f32") {
|
|
return Ok(None);
|
|
}
|
|
if data.is_empty() || !data.len().is_multiple_of(4) {
|
|
return Ok(None);
|
|
}
|
|
let posts = data.len() / 4;
|
|
let side = (posts as f64).sqrt() as usize;
|
|
if side == 0 || side.checked_mul(side) != Some(posts) {
|
|
return Ok(None);
|
|
}
|
|
let mut terrain = Vec::with_capacity(posts);
|
|
for bytes in data.chunks_exact(4) {
|
|
let value = f32::from_le_bytes(
|
|
bytes
|
|
.try_into()
|
|
.map_err(|_| archive_error("terrain float"))?,
|
|
);
|
|
if !value.is_finite() {
|
|
return Err(archive_error("terrain non-finite height"));
|
|
}
|
|
terrain.push(value.clamp(0.0, 255.0));
|
|
}
|
|
Ok(Some(terrain))
|
|
}
|
|
|
|
pub(crate) fn load_terrain(
|
|
path: &str,
|
|
data: &[u8],
|
|
callback: TerrainCallback,
|
|
bytes_read: i64,
|
|
total_bytes: i64,
|
|
) -> Result<bool, Error> {
|
|
let Some(flat) = decode_terrain(path, data)? else {
|
|
return Ok(false);
|
|
};
|
|
let side = (flat.len() as f64).sqrt() as usize;
|
|
let rows = flat.chunks_exact(side).map(<[f32]>::to_vec).collect();
|
|
callback(rows, bytes_read, total_bytes);
|
|
Ok(true)
|
|
}
|
|
|
|
pub(crate) fn save_terrain(sim: crate::Simulator, terrain_path: String) -> Result<(), Error> {
|
|
let root = PathBuf::from(terrain_path);
|
|
if root.exists() {
|
|
fs::remove_dir_all(&root).map_err(io_error)?;
|
|
}
|
|
fs::create_dir_all(&root).map_err(io_error)?;
|
|
let name = sim.name.clone();
|
|
let safe_name = name.replace(['/', '\\'], "_");
|
|
let mut file = File::create(root.join(format!("{safe_name}.r32"))).map_err(io_error)?;
|
|
for y in 0..sim.size_y {
|
|
for x in 0..sim.size_x {
|
|
let mut height = 0.0;
|
|
if !sim.terrain_height_at_point(x as i32, y as i32, &mut height)? {
|
|
height = 0.0;
|
|
}
|
|
file.write_all(&height.to_le_bytes()).map_err(io_error)?;
|
|
}
|
|
}
|
|
file.flush().map_err(io_error)
|
|
}
|
|
|
|
fn has_region_flag(flags: crate::RegionFlags, flag: crate::RegionFlags) -> bool {
|
|
flags.0 & flag.0 == flag.0
|
|
}
|
|
|
|
pub(crate) fn save_region_settings(
|
|
sim: crate::Simulator,
|
|
settings_path: String,
|
|
) -> Result<(), Error> {
|
|
let root = PathBuf::from(&settings_path);
|
|
if root.exists() {
|
|
let metadata = fs::symlink_metadata(&root).map_err(io_error)?;
|
|
if metadata.file_type().is_symlink() || !metadata.is_dir() {
|
|
return Err(archive_error("settings output root"));
|
|
}
|
|
fs::remove_dir_all(&root).map_err(io_error)?;
|
|
}
|
|
fs::create_dir_all(&root).map_err(io_error)?;
|
|
let flags = sim.flags;
|
|
let settings = RegionSettings {
|
|
allow_damage: has_region_flag(flags, crate::RegionFlags::ALLOW_DAMAGE),
|
|
allow_land_resell: !has_region_flag(flags, crate::RegionFlags::BLOCK_LAND_RESELL),
|
|
block_fly: has_region_flag(flags, crate::RegionFlags::NO_FLY),
|
|
block_land_show_in_search: has_region_flag(flags, crate::RegionFlags::BLOCK_PARCEL_SEARCH),
|
|
block_terraform: has_region_flag(flags, crate::RegionFlags::BLOCK_TERRAFORM),
|
|
disable_collisions: has_region_flag(flags, crate::RegionFlags::SKIP_COLLISIONS),
|
|
disable_physics: has_region_flag(flags, crate::RegionFlags::SKIP_PHYSICS),
|
|
disable_scripts: has_region_flag(flags, crate::RegionFlags::SKIP_SCRIPTS),
|
|
fixed_sun: has_region_flag(flags, crate::RegionFlags::SUN_FIXED),
|
|
maturity_rating: i32::from(sim.access.0),
|
|
restrict_pushing: has_region_flag(flags, crate::RegionFlags::RESTRICT_PUSH_OBJECT),
|
|
terrain_detail0: sim.terrain_detail0,
|
|
terrain_detail1: sim.terrain_detail1,
|
|
terrain_detail2: sim.terrain_detail2,
|
|
terrain_detail3: sim.terrain_detail3,
|
|
terrain_height_range00: sim.terrain_height_range00,
|
|
terrain_height_range01: sim.terrain_height_range01,
|
|
terrain_height_range10: sim.terrain_height_range10,
|
|
terrain_height_range11: sim.terrain_height_range11,
|
|
terrain_start_height00: sim.terrain_start_height00,
|
|
terrain_start_height01: sim.terrain_start_height01,
|
|
terrain_start_height10: sim.terrain_start_height10,
|
|
terrain_start_height11: sim.terrain_start_height11,
|
|
water_height: sim.water_height,
|
|
..RegionSettings::default()
|
|
};
|
|
let safe_name = sim.name.replace(['/', '\\'], "_");
|
|
settings.to_xml(
|
|
root.join(format!("{safe_name}.xml"))
|
|
.to_string_lossy()
|
|
.into_owned(),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
BLOCK, MAX_ENTRY_BYTES, MAX_PATH_DEPTH, TarArchiveReader, padded_octal,
|
|
validate_archive_path,
|
|
};
|
|
use crate::assets::TarArchiveReaderTarEntryType;
|
|
|
|
#[test]
|
|
fn rejects_absolute_parent_and_windows_style_archive_paths() {
|
|
for path in [
|
|
"/etc/passwd",
|
|
"../escape",
|
|
"assets/../../escape",
|
|
"C:\\escape",
|
|
"assets\\file",
|
|
] {
|
|
assert!(
|
|
validate_archive_path(path).is_err(),
|
|
"accepted unsafe path {path}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_archive_paths_over_depth_limit() {
|
|
let path = std::iter::repeat_n("directory", MAX_PATH_DEPTH + 1)
|
|
.collect::<Vec<_>>()
|
|
.join("/");
|
|
assert!(validate_archive_path(&path).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn octal_fields_are_fixed_width_and_nul_terminated() {
|
|
let encoded = padded_octal(0o1234, 8).expect("octal field");
|
|
assert_eq!(encoded.len(), 8);
|
|
assert_eq!(encoded.last(), Some(&0));
|
|
assert_eq!(&encoded[3..7], b"1234");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_declared_entry_over_size_limit_before_reading_payload() {
|
|
let mut header = [0u8; BLOCK];
|
|
header[..9].copy_from_slice(b"large.bin");
|
|
header[100..108].copy_from_slice(&padded_octal(0o644, 8).expect("mode"));
|
|
header[124..136].copy_from_slice(
|
|
&padded_octal((MAX_ENTRY_BYTES + 1) as u64, 12).expect("oversized length"),
|
|
);
|
|
header[148..156].fill(b' ');
|
|
header[156] = b'0';
|
|
header[257..263].copy_from_slice(b"ustar\0");
|
|
header[263..265].copy_from_slice(b"00");
|
|
let checksum = header.iter().map(|byte| u64::from(*byte)).sum();
|
|
header[148..156].copy_from_slice(&padded_octal(checksum, 8).expect("checksum"));
|
|
let reader = TarArchiveReader::new(Box::new(std::io::Cursor::new(header.to_vec())))
|
|
.expect("tar reader");
|
|
let mut path = String::new();
|
|
let mut kind = TarArchiveReaderTarEntryType::TYPE_UNKNOWN;
|
|
assert!(reader.read_entry(&mut path, &mut kind).is_err());
|
|
}
|
|
}
|