use std::any::{Any, TypeId};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::io::{self, Read, Seek};
use std::marker::PhantomData;
use std::ops::{Deref, Range};
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use crate::{tf, usdz};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ResolvedPath(PathBuf);
impl ResolvedPath {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self(path.into())
}
pub fn is_empty(&self) -> bool {
self.0.as_os_str().is_empty()
}
pub(crate) fn extension(&self) -> String {
extension(&self.0.to_string_lossy()).to_ascii_lowercase()
}
}
impl Deref for ResolvedPath {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl fmt::Display for ResolvedPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.display())
}
}
impl AsRef<Path> for ResolvedPath {
fn as_ref(&self) -> &Path {
self
}
}
#[derive(Debug, Default, Clone)]
pub struct AssetInfo {
pub version: String,
pub asset_name: String,
pub repo_path: String,
pub resolver_info: HashMap<String, String>,
}
pub trait Asset: Read + Seek + Send {
fn size(&self) -> io::Result<u64>;
fn read_all(&mut self) -> io::Result<Vec<u8>> {
let mut buf = Vec::with_capacity(usize::try_from(self.size()?).unwrap_or(0));
self.read_to_end(&mut buf)?;
Ok(buf)
}
fn shared_buffer(&self) -> Option<SharedBuffer> {
None
}
fn into_buffer(mut self: Box<Self>) -> io::Result<AssetBuffer> {
match self.shared_buffer() {
Some(view) => Ok(AssetBuffer::Shared(view)),
None => self.read_all().map(AssetBuffer::Owned),
}
}
}
#[derive(Debug, Clone)]
pub enum AssetBuffer {
Owned(Vec<u8>),
Shared(SharedBuffer),
}
impl AssetBuffer {
pub fn into_shared(self) -> SharedBuffer {
match self {
AssetBuffer::Owned(bytes) => SharedBuffer::new(bytes),
AssetBuffer::Shared(view) => view,
}
}
}
impl Deref for AssetBuffer {
type Target = [u8];
fn deref(&self) -> &[u8] {
match self {
AssetBuffer::Owned(bytes) => bytes,
AssetBuffer::Shared(view) => view,
}
}
}
impl AsRef<[u8]> for AssetBuffer {
fn as_ref(&self) -> &[u8] {
self
}
}
impl From<&'static [u8]> for AssetBuffer {
fn from(bytes: &'static [u8]) -> Self {
AssetBuffer::Shared(SharedBuffer::new(bytes))
}
}
impl From<Vec<u8>> for AssetBuffer {
fn from(bytes: Vec<u8>) -> Self {
AssetBuffer::Owned(bytes)
}
}
impl From<Arc<[u8]>> for AssetBuffer {
fn from(bytes: Arc<[u8]>) -> Self {
AssetBuffer::Shared(SharedBuffer::new(bytes))
}
}
impl From<SharedBuffer> for AssetBuffer {
fn from(view: SharedBuffer) -> Self {
AssetBuffer::Shared(view)
}
}
#[derive(Clone)]
pub struct SharedBuffer {
source: Arc<dyn SharedSource>,
range: Range<usize>,
}
impl SharedBuffer {
pub fn new(source: impl SharedSource + 'static) -> Self {
let len = source.bytes().len();
SharedBuffer {
source: Arc::new(source),
range: 0..len,
}
}
pub fn slice(&self, range: Range<usize>) -> Option<SharedBuffer> {
self.get(range.clone())?;
Some(SharedBuffer {
source: Arc::clone(&self.source),
range: self.range.start + range.start..self.range.start + range.end,
})
}
pub fn advise(&self, range: Range<usize>, advice: Advice) {
let end = range.end.min(self.len());
if range.start < end {
self.source
.advise(self.range.start + range.start..self.range.start + end, advice);
}
}
pub fn is_mapping(&self) -> bool {
self.source.is_mapping()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Advice {
Normal,
Random,
WillNeed,
}
impl Deref for SharedBuffer {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.source.bytes()[self.range.clone()]
}
}
impl AsRef<[u8]> for SharedBuffer {
fn as_ref(&self) -> &[u8] {
self
}
}
impl fmt::Debug for SharedBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SharedBuffer")
.field("range", &self.range)
.field("source_len", &self.source.bytes().len())
.finish()
}
}
pub trait SharedSource: sealed::Sealed + Send + Sync {
fn bytes(&self) -> &[u8];
fn advise(&self, _range: Range<usize>, _advice: Advice) {}
fn is_mapping(&self) -> bool {
false
}
}
impl SharedSource for &'static [u8] {
fn bytes(&self) -> &[u8] {
self
}
}
impl SharedSource for Vec<u8> {
fn bytes(&self) -> &[u8] {
self
}
}
impl SharedSource for Arc<[u8]> {
fn bytes(&self) -> &[u8] {
self
}
}
#[cfg(feature = "mmap")]
impl SharedSource for memmap2::Mmap {
fn bytes(&self) -> &[u8] {
self
}
#[cfg_attr(not(unix), allow(unused_variables))]
fn advise(&self, range: Range<usize>, advice: Advice) {
#[cfg(unix)]
{
let advice = match advice {
Advice::Normal => memmap2::Advice::Normal,
Advice::Random => memmap2::Advice::Random,
Advice::WillNeed => memmap2::Advice::WillNeed,
};
let _ = self.advise_range(advice, range.start, range.len());
}
}
fn is_mapping(&self) -> bool {
true
}
}
mod sealed {
use std::sync::Arc;
pub trait Sealed {}
impl Sealed for &'static [u8] {}
impl Sealed for Vec<u8> {}
impl Sealed for Arc<[u8]> {}
#[cfg(feature = "mmap")]
impl Sealed for memmap2::Mmap {}
}
impl Asset for fs::File {
fn size(&self) -> io::Result<u64> {
self.metadata().map(|m| m.len())
}
}
impl Asset for io::Cursor<Vec<u8>> {
fn size(&self) -> io::Result<u64> {
Ok(self.get_ref().len() as u64)
}
fn into_buffer(self: Box<Self>) -> io::Result<AssetBuffer> {
Ok(AssetBuffer::Owned(self.into_inner()))
}
}
impl Asset for io::Cursor<AssetBuffer> {
fn size(&self) -> io::Result<u64> {
Ok(self.get_ref().len() as u64)
}
fn shared_buffer(&self) -> Option<SharedBuffer> {
match self.get_ref() {
AssetBuffer::Shared(view) => Some(view.clone()),
AssetBuffer::Owned(_) => None,
}
}
fn into_buffer(self: Box<Self>) -> io::Result<AssetBuffer> {
Ok(self.into_inner())
}
}
pub trait Resolver {
fn create_identifier(&self, asset_path: &str, anchor: Option<&ResolvedPath>) -> String;
fn resolve(&self, asset_path: &str) -> Option<ResolvedPath>;
fn resolve_for_new_asset(&self, asset_path: &str) -> Option<ResolvedPath>;
fn open_asset(&self, resolved_path: &ResolvedPath) -> io::Result<Box<dyn Asset>>;
fn get_extension<'a>(&self, asset_path: &'a str) -> &'a str {
extension(asset_path)
}
fn is_context_dependent_path(&self, _asset_path: &str) -> bool {
false
}
fn in_memory_identifier<'a>(&self, asset_path: &'a str) -> Cow<'a, str> {
Cow::Borrowed(asset_path)
}
fn get_asset_info(&self, _asset_path: &str, _resolved_path: &ResolvedPath) -> AssetInfo {
AssetInfo::default()
}
fn get_modification_timestamp(&self, _asset_path: &str, resolved_path: &ResolvedPath) -> Option<SystemTime> {
fs::metadata(&**resolved_path).and_then(|m| m.modified()).ok()
}
fn identity(&self) -> String {
String::new()
}
fn cache_id(&self) -> Option<u64> {
None
}
}
pub struct CacheScope {
handle: CacheHandle,
thread: PhantomData<*const ()>,
}
impl CacheScope {
pub fn begin() -> Self {
Self::begin_shared(&Self::current().unwrap_or_default())
}
pub fn begin_shared(handle: &CacheHandle) -> Self {
SCOPES.with_borrow_mut(|scopes| scopes.push(handle.clone()));
CacheScope {
handle: handle.clone(),
thread: PhantomData,
}
}
pub fn handle(&self) -> CacheHandle {
self.handle.clone()
}
pub fn current() -> Option<CacheHandle> {
SCOPES.with_borrow(|scopes| scopes.last().cloned())
}
}
impl Drop for CacheScope {
fn drop(&mut self) {
let _ = SCOPES.try_with(|scopes| {
let mut scopes = scopes.borrow_mut();
if let Some(at) = scopes.iter().rposition(|open| Arc::ptr_eq(&open.0, &self.handle.0)) {
scopes.remove(at);
}
});
}
}
thread_local! {
static SCOPES: RefCell<Vec<CacheHandle>> = const { RefCell::new(Vec::new()) };
}
#[derive(Clone, Default)]
pub struct CacheHandle(Arc<Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>);
impl CacheHandle {
pub fn cache<T: Any + Default + Send + Sync>(&self) -> Arc<T> {
let mut caches = tf::lock(&self.0);
let cache = caches
.entry(TypeId::of::<T>())
.or_insert_with(|| Arc::new(T::default()));
Arc::clone(cache)
.downcast()
.expect("a cache is kept under its own type")
}
}
pub struct DefaultResolver {
search_paths: Vec<PathBuf>,
id: u64,
#[cfg(feature = "mmap")]
map_files: bool,
}
impl DefaultResolver {
pub fn new() -> Self {
Self {
search_paths: Vec::new(),
id: crate::next_id(),
#[cfg(feature = "mmap")]
map_files: false,
}
}
#[cfg(feature = "mmap")]
#[allow(unsafe_code)]
pub unsafe fn map_files(mut self) -> Self {
self.map_files = true;
self
}
pub fn with_search_paths(paths: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
let mut resolver = Self::new();
resolver.search_paths = paths.into_iter().map(|p| normalize_search_path(p.into())).collect();
resolver
}
fn resolve_with_search_paths(&self, asset_path: &str) -> Option<ResolvedPath> {
let rel_path = &*host_path(asset_path)?;
if rel_path.is_absolute() {
return if rel_path.exists() {
Some(ResolvedPath::new(rel_path.canonicalize().ok()?))
} else {
None
};
}
for dir in &self.search_paths {
let candidate = dir.join(rel_path);
if candidate.exists() {
return Some(ResolvedPath::new(candidate.canonicalize().ok()?));
}
}
if let Ok(cwd) = std::env::current_dir() {
let candidate = cwd.join(rel_path);
if candidate.exists() {
return Some(ResolvedPath::new(candidate.canonicalize().ok()?));
}
}
None
}
}
impl Default for DefaultResolver {
fn default() -> Self {
Self::new()
}
}
impl Resolver for DefaultResolver {
fn create_identifier(&self, asset_path: &str, anchor: Option<&ResolvedPath>) -> String {
if asset_path.is_empty() {
return String::new();
}
if let Some((package, inner)) = split_authored_package(asset_path) {
let anchored = self.create_identifier(&package, anchor);
return nest_packaged_path(&anchored, &inner);
}
let Some(path) = host_path(asset_path) else {
return asset_path.to_string();
};
if path.is_absolute() {
return canonical_identifier(path.into_owned());
}
if let Some(anchor) = anchor {
let anchor_str = anchor.to_string_lossy();
if is_package_relative_path(&anchor_str)
&& let Some((package, inner)) = split_package_relative_path_inner(&anchor_str)
{
let inner_dir = inner.rsplit_once('/').map_or("", |(dir, _)| dir);
let joined = join_packaged_path(inner_dir, asset_path);
return nest_packaged_path(&package, &joined);
}
if anchor_str.to_ascii_lowercase().ends_with(".usdz") {
let joined = join_packaged_path("", asset_path);
return join_package_relative_path(&anchor_str, &joined);
}
if let Some(dir) = anchor.parent() {
let anchored = dir.join(&path);
if is_search_path(asset_path) {
return match anchored.canonicalize() {
Ok(found) => found.to_string_lossy().into_owned(),
Err(_) => path.to_string_lossy().into_owned(),
};
}
return canonical_identifier(anchored);
}
}
if let Ok(cwd) = std::env::current_dir() {
return canonical_identifier(cwd.join(path));
}
asset_path.to_string()
}
fn resolve(&self, asset_path: &str) -> Option<ResolvedPath> {
if asset_path.is_empty() {
return None;
}
if let Some((package, inner)) = split_authored_package(asset_path) {
let resolved_package = self.resolve_with_search_paths(&package)?;
let package_str = resolved_package.to_string_lossy();
let _scope = is_package_relative_path(&inner).then(CacheScope::begin);
if !usdz::package_contains(self, &package_str, &inner) {
return None;
}
return Some(ResolvedPath::new(join_package_relative_path(&package_str, &inner)));
}
self.resolve_with_search_paths(asset_path)
}
fn resolve_for_new_asset(&self, asset_path: &str) -> Option<ResolvedPath> {
if asset_path.is_empty() {
return None;
}
if let Some((package, inner)) = split_authored_package(asset_path) {
let package = self.resolve_for_new_asset(&package)?;
return Some(ResolvedPath::new(join_package_relative_path(
&package.to_string_lossy(),
&inner,
)));
}
let path = host_path(asset_path)?;
if path.is_absolute() {
return Some(ResolvedPath::new(path.into_owned()));
}
std::env::current_dir()
.ok()
.map(|cwd| ResolvedPath::new(cwd.join(path)))
}
fn is_context_dependent_path(&self, asset_path: &str) -> bool {
is_search_path(asset_path)
}
fn in_memory_identifier<'a>(&self, asset_path: &'a str) -> Cow<'a, str> {
match split_authored_package(asset_path) {
Some((package, inner)) => Cow::Owned(join_package_relative_path(&normalize_if_relative(&package), &inner)),
None => normalize_if_relative(asset_path),
}
}
fn identity(&self) -> String {
self.search_paths.iter().map(|p| format!("{p:?}")).collect()
}
fn cache_id(&self) -> Option<u64> {
Some(self.id)
}
fn open_asset(&self, resolved_path: &ResolvedPath) -> io::Result<Box<dyn Asset>> {
let path_str = resolved_path.to_str().unwrap_or_default();
if is_package_relative_path(path_str) {
let (package, inner) = split_package_relative_path_outer(path_str).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid package-relative path: {}", resolved_path),
)
})?;
let bytes = usdz::package_entry(self, &package, &inner)?.into_shared();
return Ok(Box::new(io::Cursor::new(AssetBuffer::Shared(bytes))));
}
self.open_file(resolved_path)
}
}
impl DefaultResolver {
#[cfg_attr(not(feature = "mmap"), allow(clippy::unused_self))]
fn open_file(&self, path: &Path) -> io::Result<Box<dyn Asset>> {
#[cfg(feature = "mmap")]
if self.map_files {
return open_mapped(path);
}
Ok(Box::new(fs::File::open(path)?))
}
}
#[cfg(feature = "mmap")]
#[allow(unsafe_code)]
fn open_mapped(path: &Path) -> io::Result<Box<dyn Asset>> {
#[cfg(windows)]
let file = {
use std::os::windows::fs::OpenOptionsExt;
fs::OpenOptions::new().read(true).share_mode(0x1 | 0x4).open(path)?
};
#[cfg(not(windows))]
let file = fs::File::open(path)?;
let Some(len) = usize::try_from(file.metadata()?.len()).ok().filter(|&len| len > 0) else {
return Ok(Box::new(file));
};
match unsafe { memmap2::MmapOptions::new().len(len).map(&file) } {
Ok(map) => Ok(Box::new(io::Cursor::new(AssetBuffer::Shared(SharedBuffer::new(map))))),
Err(_) => Ok(Box::new(file)),
}
}
fn lexically_normalize(path: &Path) -> PathBuf {
path.components().filter(|c| !matches!(c, Component::CurDir)).collect()
}
const SEPARATORS: [char; 2] = ['/', '\\'];
fn is_relative_path(asset_path: &str) -> bool {
let drive = matches!(asset_path.as_bytes(), [letter, b':', ..] if letter.is_ascii_alphabetic());
!asset_path.starts_with(SEPARATORS) && !drive
}
pub(crate) fn is_file_relative_path(asset_path: &str) -> bool {
matches!(asset_path.split(SEPARATORS).next(), Some("." | ".."))
}
fn is_search_path(asset_path: &str) -> bool {
is_relative_path(asset_path) && !is_file_relative_path(asset_path)
}
fn normalize_asset_path(asset_path: &str) -> Cow<'_, str> {
let dropped = |segment: &str| segment.is_empty() || segment == ".";
if !asset_path.contains('\\') && !asset_path.split('/').any(dropped) {
return Cow::Borrowed(asset_path);
}
let kept: Vec<&str> = asset_path
.split(SEPARATORS)
.filter(|segment| !dropped(segment))
.collect();
Cow::Owned(kept.join("/"))
}
fn normalize_if_relative(asset_path: &str) -> Cow<'_, str> {
if is_relative_path(asset_path) {
normalize_asset_path(asset_path)
} else {
Cow::Borrowed(asset_path)
}
}
fn host_path(asset_path: &str) -> Option<Cow<'_, Path>> {
if is_relative_path(asset_path) {
return Some(match normalize_asset_path(asset_path) {
Cow::Borrowed(path) => Cow::Borrowed(Path::new(path)),
Cow::Owned(path) => Cow::Owned(PathBuf::from(path)),
});
}
let path = Path::new(asset_path);
matches!(
path.components().next(),
Some(Component::Prefix(_) | Component::RootDir)
)
.then_some(Cow::Borrowed(path))
}
fn canonical_identifier(path: PathBuf) -> String {
path.canonicalize()
.unwrap_or_else(|_| lexically_normalize(&path))
.to_string_lossy()
.into_owned()
}
fn normalize_search_path(path: PathBuf) -> PathBuf {
let path = if path.is_relative() {
std::env::current_dir().unwrap_or_default().join(path)
} else {
path
};
lexically_normalize(&path)
}
pub fn is_package_relative_path(path: &str) -> bool {
path.contains('[') && path.ends_with(']')
}
pub fn split_package_relative_path_outer(path: &str) -> Option<(String, String)> {
let bracket = path.find('[')?;
if !path.ends_with(']') {
return None;
}
let outer = &path[..bracket];
let inner = &path[bracket + 1..path.len() - 1];
Some((outer.to_string(), inner.to_string()))
}
pub fn split_package_relative_path_inner(path: &str) -> Option<(String, String)> {
if !path.ends_with(']') {
return None;
}
let open = path.rfind('[')?;
let close = path[open..].find(']').map(|i| open + i)?;
let inner = &path[open + 1..close];
let mut outer = path[..open].to_string();
let remainder = &path[close + 1..];
outer.push_str(remainder);
Some((outer, inner.to_string()))
}
pub fn join_package_relative_path(package_path: &str, packaged_path: &str) -> String {
format!("{}[{}]", package_path, packaged_path)
}
fn innermost_span(path: &str) -> Range<usize> {
if !is_package_relative_path(path) {
return 0..path.len();
}
let Some(open) = path.rfind('[') else {
return 0..path.len();
};
let close = path[open..].find(']').map_or(path.len(), |i| open + i);
open + 1..close
}
pub(crate) fn extension(path: &str) -> &str {
Path::new(&path[innermost_span(path)])
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
}
pub(crate) fn split_file_name(path: &str) -> (&str, &str) {
let span = innermost_span(path);
let inner = &path[span.clone()];
let name = inner.rfind(SEPARATORS).map_or(0, |i| i + 1);
(&path[..span.start + name], &inner[name..])
}
fn join_packaged_path(dir: &str, rel: &str) -> String {
walk_packaged_path(dir, rel).0
}
pub(crate) fn join_within_package(dir: &str, rel: &str) -> Option<String> {
let (joined, climbed_out) = walk_packaged_path(dir, rel);
(!climbed_out).then_some(joined)
}
fn walk_packaged_path(dir: &str, rel: &str) -> (String, bool) {
let mut components: Vec<&str> = dir.split('/').filter(|c| !c.is_empty()).collect();
let mut climbed_out = false;
for part in rel.split(SEPARATORS) {
match part {
"" | "." => {}
".." => climbed_out |= components.pop().is_none(),
other => components.push(other),
}
}
(components.join("/"), climbed_out)
}
fn split_authored_package(asset_path: &str) -> Option<(String, String)> {
let (package, inner) = split_package_relative_path_outer(asset_path)?;
Some((package, normalize_packaged_path(&inner)))
}
fn normalize_packaged_path(packaged: &str) -> String {
let entry = |level: &str| join_within_package("", level).unwrap_or_else(|| level.to_owned());
match split_authored_package(packaged) {
Some((package, inner)) => join_package_relative_path(&entry(&package), &inner),
None => entry(packaged),
}
}
pub(crate) fn nest_packaged_path(base: &str, leaf: &str) -> String {
match split_package_relative_path_outer(base) {
Some((package, inner)) => join_package_relative_path(&package, &nest_packaged_path(&inner, leaf)),
None => join_package_relative_path(base, leaf),
}
}
#[cfg(test)]
pub(crate) mod tests {
use std::thread;
use super::*;
pub(crate) struct TestResolver<F>(pub(crate) F);
impl<F: Fn() -> io::Result<Box<dyn Asset>>> Resolver for TestResolver<F> {
fn create_identifier(&self, asset_path: &str, _anchor: Option<&ResolvedPath>) -> String {
asset_path.to_string()
}
fn resolve(&self, asset_path: &str) -> Option<ResolvedPath> {
Some(ResolvedPath::new(asset_path))
}
fn resolve_for_new_asset(&self, asset_path: &str) -> Option<ResolvedPath> {
Some(ResolvedPath::new(asset_path))
}
fn open_asset(&self, _resolved_path: &ResolvedPath) -> io::Result<Box<dyn Asset>> {
(self.0)()
}
}
#[test]
fn resolved_path_empty() {
let p = ResolvedPath::new("");
assert!(p.is_empty());
}
#[test]
fn resolved_path_display() {
let p = ResolvedPath::new("/tmp/model.usda");
assert_eq!(format!("{}", p), "/tmp/model.usda");
assert!(!p.is_empty());
}
#[test]
fn resolved_path_deref() {
let p = ResolvedPath::new("some/path/model.usda");
assert_eq!(p.extension(), "usda");
assert_eq!(p.file_name().unwrap(), "model.usda");
assert_eq!(ResolvedPath::new("x/Model.USDZ").extension(), "usdz");
assert_eq!(ResolvedPath::new("x/noext").extension(), "");
}
#[test]
fn resolver_identity_renders_search_paths() {
assert!(DefaultResolver::new().identity().is_empty());
let a = DefaultResolver::with_search_paths(["/show/assets", "/lib"]);
let b = DefaultResolver::with_search_paths(["/show/assets", "/lib"]);
let c = DefaultResolver::with_search_paths(["/other"]);
assert_eq!(a.identity(), b.identity());
assert_ne!(a.identity(), c.identity());
let cwd = std::env::current_dir().unwrap();
let rel = DefaultResolver::with_search_paths(["assets"]);
let abs = DefaultResolver::with_search_paths([cwd.join("assets")]);
assert_eq!(rel.identity(), abs.identity());
let plain = DefaultResolver::with_search_paths([cwd.join("lib")]);
let noisy = DefaultResolver::with_search_paths([cwd.join("lib").join(".")]);
assert_eq!(plain.identity(), noisy.identity());
let embedded = DefaultResolver::with_search_paths([cwd.join("a\nb")]);
let split = DefaultResolver::with_search_paths([cwd.join("a"), cwd.join("b")]);
assert_ne!(embedded.identity(), split.identity());
}
#[test]
fn is_package_relative() {
assert!(is_package_relative_path("Model.usdz[Geom.usd]"));
assert!(is_package_relative_path("A.usdz[B.usdz[C.usd]]"));
assert!(!is_package_relative_path("Model.usdz"));
assert!(!is_package_relative_path("Model.usdz["));
assert!(!is_package_relative_path("plain/path.usd"));
}
#[test]
fn split_outer_simple() {
let result = split_package_relative_path_outer("Model.usdz[Geom.usd]");
assert_eq!(result, Some(("Model.usdz".to_string(), "Geom.usd".to_string())));
}
#[test]
fn split_outer_nested() {
let result = split_package_relative_path_outer("Outer.usdz[Inner.usdz[Geom.usd]]");
assert_eq!(
result,
Some(("Outer.usdz".to_string(), "Inner.usdz[Geom.usd]".to_string()))
);
}
#[test]
fn split_inner_simple() {
let result = split_package_relative_path_inner("Model.usdz[Geom.usd]");
assert_eq!(result, Some(("Model.usdz".to_string(), "Geom.usd".to_string())));
}
#[test]
fn split_inner_nested() {
let result = split_package_relative_path_inner("Outer.usdz[Inner.usdz[Geom.usd]]");
assert_eq!(
result,
Some(("Outer.usdz[Inner.usdz]".to_string(), "Geom.usd".to_string()))
);
}
#[test]
fn split_invalid() {
assert_eq!(split_package_relative_path_outer("no_brackets"), None);
assert_eq!(split_package_relative_path_inner("no_brackets"), None);
assert_eq!(split_package_relative_path_outer("open[only"), None);
assert_eq!(split_package_relative_path_inner("open[only"), None);
}
#[test]
fn join_package_path() {
assert_eq!(
join_package_relative_path("Model.usdz", "Geom.usd"),
"Model.usdz[Geom.usd]"
);
}
#[test]
fn resolver_empty_path() {
let resolver = DefaultResolver::new();
assert_eq!(resolver.resolve(""), None);
assert_eq!(resolver.create_identifier("", None), "");
}
#[test]
fn resolver_extension() {
let resolver = DefaultResolver::new();
assert_eq!(resolver.get_extension("model.usda"), "usda");
assert_eq!(resolver.get_extension("archive.usdz"), "usdz");
assert_eq!(resolver.get_extension("no_extension"), "");
assert_eq!(resolver.get_extension("path/to/file.usdc"), "usdc");
assert_eq!(resolver.get_extension("pkg.usdz[inner.usdz[a.usda]]"), "usda");
}
#[test]
fn resolver_resolve_existing_file() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let resolver = DefaultResolver::with_search_paths([&manifest]);
let resolved = resolver.resolve("Cargo.toml");
assert!(resolved.is_some());
let resolved = resolved.unwrap();
assert!(!resolved.is_empty());
assert!(resolved.exists());
}
#[test]
fn resolver_resolve_nonexistent() {
let resolver = DefaultResolver::new();
assert_eq!(resolver.resolve("nonexistent_file_12345.usda"), None);
}
#[test]
fn resolver_resolve_absolute_path() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let abs_path = Path::new(&manifest).join("Cargo.toml");
let resolver = DefaultResolver::new();
let resolved = resolver.resolve(abs_path.to_str().unwrap());
assert!(resolved.is_some());
}
#[test]
fn resolver_create_identifier_absolute() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let abs_path = Path::new(&manifest).join("Cargo.toml");
let abs_str = abs_path.to_str().unwrap();
let resolver = DefaultResolver::new();
let id = resolver.create_identifier(abs_str, None);
assert!(!id.is_empty());
}
#[test]
fn resolver_create_identifier_anchored() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let anchor = ResolvedPath::new(PathBuf::from(&manifest).join("src/lib.rs"));
let resolver = DefaultResolver::new();
let id = resolver.create_identifier("ar.rs", Some(&anchor));
assert!(id.contains("ar.rs"));
}
#[test]
fn resolver_create_identifier_dot_relative() {
let resolver = DefaultResolver::new();
let anchor = ResolvedPath::new(PathBuf::from("root.usda"));
assert_eq!(
resolver.create_identifier("./sub.usda", Some(&anchor)),
resolver.create_identifier("sub.usda", Some(&anchor)),
"a leading `./` normalizes away when the target cannot be canonicalized"
);
}
#[test]
fn search_path_identifier_falls_back() {
let dir = tempfile::tempdir().unwrap();
let scene = dir.path().join("scene");
let library = dir.path().join("library");
fs::create_dir_all(scene.join("lib")).unwrap();
fs::create_dir_all(library.join("lib")).unwrap();
fs::write(scene.join("root.usda"), "#usda 1.0\n").unwrap();
fs::write(library.join("lib").join("sub.usda"), "#usda 1.0\n").unwrap();
let resolver = DefaultResolver::with_search_paths([&library]);
let anchor = ResolvedPath::new(scene.join("root.usda"));
let id = resolver.create_identifier("lib/sub.usda", Some(&anchor));
assert_eq!(id, "lib/sub.usda", "a search path keeps its `/`-spelled bare form");
assert_eq!(
resolver.create_identifier("lib/./sub.usda", Some(&anchor)),
"lib/sub.usda",
"interior `.` segments normalize away"
);
let resolved = resolver.resolve(&id).expect("found through the search directory");
assert_eq!(
resolved.canonicalize().unwrap(),
library.join("lib/sub.usda").canonicalize().unwrap()
);
let dot = resolver.create_identifier("./lib/sub.usda", Some(&anchor));
assert!(Path::new(&dot).is_absolute(), "file-relative path anchored: {dot}");
assert!(resolver.resolve(&dot).is_none());
fs::write(scene.join("lib").join("sub.usda"), "#usda 1.0\n").unwrap();
let beside = resolver.create_identifier("lib/sub.usda", Some(&anchor));
assert_eq!(
Path::new(&beside).canonicalize().unwrap(),
scene.join("lib/sub.usda").canonicalize().unwrap()
);
}
#[test]
fn search_path_predicate() {
assert!(is_search_path("lib/sub.usda"));
assert!(is_search_path("sub.usda"));
assert!(is_search_path(r"tex\a.png"));
assert!(!is_search_path("./sub.usda"));
assert!(!is_search_path("../sub.usda"));
assert!(!is_search_path("/abs/sub.usda"));
assert!(!is_search_path(r".\sub.usda"));
assert!(!is_search_path(r"..\sub.usda"));
for absolute in [
r"C:\a.usda",
"C:/a.usda",
r"\\server\a.usda",
r"\\?\C:\a.usda",
"a:b.usda",
] {
assert!(!is_relative_path(absolute), "{absolute}");
}
assert!(is_search_path("ab:c.usda"));
assert!(is_search_path("1:c.usda"));
}
#[test]
fn asset_path_normalized() {
assert_eq!(&*normalize_asset_path("./sub.usda"), "sub.usda");
assert_eq!(&*normalize_asset_path("././sub.usda"), "sub.usda");
assert_eq!(&*normalize_asset_path("dir/./sub.usda"), "dir/sub.usda");
assert_eq!(&*normalize_asset_path(r"tex\stone.png"), "tex/stone.png");
assert_eq!(&*normalize_asset_path(r".\sub.usda"), "sub.usda");
assert_eq!(&*normalize_asset_path(r"dir\.\sub.usda"), "dir/sub.usda");
assert_eq!(&*normalize_asset_path("a//b"), "a/b");
assert_eq!(&*normalize_asset_path(r"..\a"), "../a");
for unchanged in ["sub.usda", "../a", "a/../b.usda"] {
assert!(
matches!(normalize_asset_path(unchanged), Cow::Borrowed(path) if path == unchanged),
"{unchanged} is borrowed as written"
);
}
}
#[test]
fn in_memory_identifier_normalized() {
let resolver = DefaultResolver::new();
assert_eq!(resolver.in_memory_identifier(r".\sub\layer.usda"), "sub/layer.usda");
assert_eq!(resolver.in_memory_identifier("/abs/./a.usda"), "/abs/./a.usda");
assert_eq!(resolver.in_memory_identifier(r"C:\dir\a.usda"), r"C:\dir\a.usda");
assert_eq!(
resolver.in_memory_identifier(r"pkg\m.usdz[in.usdz[tex\a.png]]"),
"pkg/m.usdz[in.usdz[tex/a.png]]"
);
assert_eq!(
resolver.in_memory_identifier(r"C:\dir\m.usdz[tex/a.png]"),
r"C:\dir\m.usdz[tex/a.png]"
);
}
#[test]
fn backslash_identifier() {
let dir = tempfile::tempdir().unwrap();
let anchor = ResolvedPath::new(dir.path().join("root.usda"));
let resolver = DefaultResolver::new();
assert_eq!(
resolver.create_identifier(r"tex\stone.png", Some(&anchor)),
"tex/stone.png",
"a missing search path keeps its bare normalized spelling"
);
let colon = resolver.create_identifier("./ab:c.usda", Some(&anchor));
assert!(Path::new(&colon).is_absolute(), "a longer prefix is no drive: {colon}");
let dot = resolver.create_identifier(r".\tex\stone.png", Some(&anchor));
assert!(Path::new(&dot).is_absolute(), "file-relative path anchored: {dot}");
assert_eq!(dot, resolver.create_identifier("./tex/stone.png", Some(&anchor)));
fs::create_dir(dir.path().join("tex")).unwrap();
fs::write(dir.path().join("tex").join("stone.png"), b"png").unwrap();
let expected = resolver.create_identifier("tex/stone.png", Some(&anchor));
assert!(Path::new(&expected).is_absolute());
assert_eq!(resolver.create_identifier(r"tex\stone.png", Some(&anchor)), expected);
assert_eq!(resolver.create_identifier(r".\tex\stone.png", Some(&anchor)), expected);
}
#[test]
fn backslash_search_dir() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("tex")).unwrap();
fs::write(dir.path().join("tex").join("stone.png"), b"png").unwrap();
let resolver = DefaultResolver::with_search_paths([dir.path()]);
let resolved = resolver
.resolve(r"tex\stone.png")
.expect("found in the search directory");
assert_eq!(
resolved.canonicalize().unwrap(),
dir.path().join("tex/stone.png").canonicalize().unwrap()
);
}
#[test]
fn backslash_new_asset() {
let resolver = DefaultResolver::new();
let forward = resolver.resolve_for_new_asset("sub/new.usda").unwrap();
assert!(forward.is_absolute());
assert_eq!(resolver.resolve_for_new_asset(r"sub\new.usda").unwrap(), forward);
assert_eq!(resolver.resolve_for_new_asset(r".\sub\new.usda").unwrap(), forward);
assert_eq!(
resolver.resolve_for_new_asset(r"sub\new.usdz[tex\a.png]").unwrap(),
resolver.resolve_for_new_asset("sub/new.usdz[tex/a.png]").unwrap()
);
}
#[cfg(unix)]
#[test]
fn foreign_absolute_unresolved() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join(r"C:\x\a.png"), b"png").unwrap();
fs::write(dir.path().join("root.usda"), "#usda 1.0\n").unwrap();
let anchor = ResolvedPath::new(dir.path().join("root.usda"));
let resolver = DefaultResolver::with_search_paths([dir.path()]);
for foreign in [r"C:\x\a.png", "C:/x/a.png", r"\\server\x\a.png"] {
assert_eq!(resolver.create_identifier(foreign, Some(&anchor)), foreign);
assert_eq!(resolver.create_identifier(foreign, None), foreign);
assert!(resolver.resolve(foreign).is_none(), "{foreign}");
assert!(resolver.resolve_for_new_asset(foreign).is_none(), "{foreign}");
}
}
#[cfg(unix)]
#[test]
fn backslash_file_name_shadowed() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("tex")).unwrap();
fs::write(dir.path().join("tex").join("stone.png"), b"nested").unwrap();
fs::write(dir.path().join(r"tex\stone.png"), b"literal").unwrap();
let anchor = ResolvedPath::new(dir.path().join("root.usda"));
let resolver = DefaultResolver::new();
let id = resolver.create_identifier(r"tex\stone.png", Some(&anchor));
let resolved = resolver.resolve(&id).expect("resolves");
assert_eq!(fs::read(&*resolved).unwrap(), b"nested");
}
#[test]
fn backslash_package_relative() {
let dir = tempfile::tempdir().unwrap();
let packages = dir.path().join("packages");
fs::create_dir(&packages).unwrap();
let mut writer = usdz::ArchiveWriter::create(packages.join("model.usdz")).unwrap();
writer.add_layer("root.usda", b"#usda 1.0\n").unwrap();
writer.add_layer("tex/stone.png", b"png").unwrap();
writer.finish().unwrap();
let anchor = ResolvedPath::new(dir.path().join("root.usda"));
let resolver = DefaultResolver::new();
let package = resolver.create_identifier("packages/model.usdz", Some(&anchor));
assert!(Path::new(&package).is_absolute());
let id = resolver.create_identifier(r"packages\model.usdz[tex\stone.png]", Some(&anchor));
assert_eq!(id, format!("{package}[tex/stone.png]"));
let resolved = resolver.resolve(&id).expect("the entry is in the archive");
assert_eq!(resolver.open_asset(&resolved).unwrap().read_all().unwrap(), b"png");
assert!(
resolver.resolve(&format!(r"{package}[tex\stone.png]")).is_some(),
"the packaged path is normalized when resolving too"
);
assert_eq!(
resolver.create_identifier(r"outer.usdz[inner.usdz[tex\stone.png]]", Some(&anchor)),
"outer.usdz[inner.usdz[tex/stone.png]]",
"a missing package keeps its bare spelling around the normalized entry"
);
assert!(
resolver.resolve(&format!("{package}[../root.usda]")).is_none(),
"a packaged path leading out of the package names no entry"
);
assert_eq!(
resolver.create_identifier(r".\outer.usdz[inner.usdz[tex\stone.png]]", Some(&anchor)),
format!(
"{}[inner.usdz[tex/stone.png]]",
resolver.create_identifier("./outer.usdz", Some(&anchor))
)
);
}
#[test]
fn resolver_create_identifier_package_relative_anchored() {
let resolver = DefaultResolver::new();
let anchor = ResolvedPath::new(PathBuf::from("/scene/root.usda"));
assert_eq!(
resolver.create_identifier("model.usdz[geom.usd]", Some(&anchor)),
join_package_relative_path(&resolver.create_identifier("model.usdz", Some(&anchor)), "geom.usd"),
);
}
#[test]
fn packaged_path_join_forward_slash() {
assert_eq!(join_packaged_path("geom", "mesh.usd"), "geom/mesh.usd");
assert_eq!(join_packaged_path("geom", "./mesh.usd"), "geom/mesh.usd");
assert_eq!(join_packaged_path("geom", "../tex/foo.usda"), "tex/foo.usda");
assert_eq!(join_packaged_path("", "geom/mesh.usd"), "geom/mesh.usd");
assert_eq!(join_packaged_path("geom", r"sub\mesh.usd"), "geom/sub/mesh.usd");
}
#[test]
fn in_package_anchor_crosses_dirs() {
let resolver = DefaultResolver::new();
let anchor = ResolvedPath::new(PathBuf::from("pkg.usdz[geom/scene.usda]"));
assert_eq!(
resolver.create_identifier("../tex/foo.usda", Some(&anchor)),
"pkg.usdz[tex/foo.usda]",
);
}
#[test]
fn nest_packaged_well_formed() {
assert_eq!(nest_packaged_path("pkg.usdz", "geom.usd"), "pkg.usdz[geom.usd]");
assert_eq!(
nest_packaged_path("pkg.usdz[model.usdz]", "geom.usd"),
"pkg.usdz[model.usdz[geom.usd]]",
);
assert_eq!(
nest_packaged_path("a.usdz[b.usdz[c.usdz]]", "d.usd"),
"a.usdz[b.usdz[c.usdz[d.usd]]]",
);
}
#[test]
fn resolver_open_asset() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let resolver = DefaultResolver::with_search_paths([&manifest]);
let resolved = resolver.resolve("Cargo.toml").unwrap();
let mut asset = resolver.open_asset(&resolved).unwrap();
let size = asset.size().unwrap();
assert!(size > 0);
let data = asset.read_all().unwrap();
assert_eq!(data.len() as u64, size);
assert!(String::from_utf8_lossy(&data).contains("[package]"));
}
#[test]
fn resolver_modification_timestamp() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let resolver = DefaultResolver::with_search_paths([&manifest]);
let resolved = resolver.resolve("Cargo.toml").unwrap();
let ts = resolver.get_modification_timestamp("Cargo.toml", &resolved);
assert!(ts.is_some());
}
#[test]
fn resolve_for_new_asset_relative() {
let resolver = DefaultResolver::new();
let resolved = resolver.resolve_for_new_asset("new_file.usda");
assert!(resolved.is_some());
let resolved = resolved.unwrap();
assert!(resolved.is_absolute());
}
#[test]
fn cursor_asset_read() {
let data = b"hello world".to_vec();
let mut asset = io::Cursor::new(data.clone());
assert_eq!(asset.size().unwrap(), 11);
let result = asset.read_all().unwrap();
assert_eq!(result, data);
}
#[test]
fn cursor_into_buffer() {
let data = b"hello world".to_vec();
let pointer = data.as_ptr();
let mut asset: Box<dyn Asset> = Box::new(io::Cursor::new(data));
assert_eq!(asset.read_all().unwrap(), b"hello world");
assert_eq!(asset.size().unwrap(), 11);
let AssetBuffer::Owned(moved) = asset.into_buffer().unwrap() else {
panic!("an owned cursor moves its buffer out");
};
assert_eq!(moved.as_ptr(), pointer);
let shared: Arc<[u8]> = b"hello world".to_vec().into();
let asset: Box<dyn Asset> = Box::new(io::Cursor::new(AssetBuffer::from(shared.clone())));
let AssetBuffer::Shared(view) = asset.into_buffer().unwrap() else {
panic!("a shared cursor shares its buffer");
};
assert_eq!(view.as_ptr(), shared.as_ptr());
}
#[test]
fn shared_slice() {
let source: Arc<[u8]> = (0..32u8).collect::<Vec<_>>().into();
let view = SharedBuffer::new(source.clone());
assert_eq!(view.as_ptr(), source.as_ptr());
let middle = view.slice(8..24).expect("within the view");
assert_eq!(middle.as_ptr(), source[8..].as_ptr());
assert_eq!(&*middle, &source[8..24]);
let inner = middle.slice(4..8).expect("within the slice");
assert_eq!(&*inner, &source[12..16]);
assert!(middle.slice(8..17).is_none());
let (start, end) = (5, 4);
assert!(middle.slice(start..end).is_none());
assert!(view.slice(32..32).is_some());
}
#[test]
fn file_asset_read() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("asset.bin");
fs::write(&path, b"hello world")?;
let asset = DefaultResolver::new().open_asset(&ResolvedPath::new(&path))?;
assert!(asset.shared_buffer().is_none());
let AssetBuffer::Owned(bytes) = asset.into_buffer()? else {
panic!("a file asset is read");
};
assert_eq!(bytes, b"hello world");
Ok(())
}
#[cfg(feature = "mmap")]
#[test]
#[allow(unsafe_code)]
fn file_asset_mapped() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("asset.bin");
fs::write(&path, b"hello world")?;
let resolver = unsafe { DefaultResolver::new().map_files() };
let asset = resolver.open_asset(&ResolvedPath::new(&path))?;
let view = asset.shared_buffer().expect("a mapped file offers its bytes");
assert_eq!(view.len() as u64, fs::metadata(&path)?.len());
assert_eq!(&*view, b"hello world");
assert!(matches!(asset.into_buffer()?, AssetBuffer::Shared(_)));
let empty = dir.path().join("empty.bin");
fs::write(&empty, b"")?;
let asset = resolver.open_asset(&ResolvedPath::new(&empty))?;
assert!(asset.shared_buffer().is_none());
assert!(matches!(asset.into_buffer()?, AssetBuffer::Owned(bytes) if bytes.is_empty()));
Ok(())
}
#[test]
fn shared_buffer_whole_asset() {
let data = b"hello world".to_vec();
let pointer = data.as_ptr();
let view = AssetBuffer::Owned(data).into_shared();
assert_eq!(view.as_ptr(), pointer);
assert_eq!(&*view, b"hello world");
let shared: Arc<[u8]> = b"hello world".to_vec().into();
let mut asset = io::Cursor::new(AssetBuffer::from(shared.clone()));
asset.seek(io::SeekFrom::Start(6)).unwrap();
let view = asset.shared_buffer().expect("a shared cursor offers its bytes");
asset.seek(io::SeekFrom::Start(0)).unwrap();
assert_eq!(&*view, asset.read_all().unwrap().as_slice());
let mut asset = io::Cursor::new(AssetBuffer::Shared(view));
assert_eq!(
asset.shared_buffer().expect("a view is offered").as_ptr(),
shared.as_ptr()
);
assert_eq!(asset.read_all().unwrap(), b"hello world");
assert!(
io::Cursor::new(AssetBuffer::Owned(Vec::new()))
.shared_buffer()
.is_none()
);
}
#[test]
fn cursor_asset_seek() {
let data = b"hello world".to_vec();
let mut asset = io::Cursor::new(data);
asset.seek(io::SeekFrom::Start(6)).unwrap();
let mut buf = [0u8; 5];
asset.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"world");
}
#[derive(Default)]
struct Kept(Mutex<Vec<u32>>);
fn kept() -> Option<Vec<u32>> {
Some(CacheScope::current()?.cache::<Kept>().0.lock().unwrap().clone())
}
fn keep(value: u32) {
let handle = CacheScope::current().expect("a scope is open");
handle.cache::<Kept>().0.lock().unwrap().push(value);
}
#[test]
fn nested_scope_shares() {
assert_eq!(kept(), None);
{
let _scope = CacheScope::begin();
keep(1);
{
let _nested = CacheScope::begin();
keep(2);
}
assert_eq!(kept(), Some(vec![1, 2]));
}
assert_eq!(kept(), None);
let _sibling = CacheScope::begin();
assert_eq!(kept(), Some(vec![]));
}
#[test]
fn scope_per_thread() {
let scope = CacheScope::begin();
keep(1);
let handle = scope.handle();
thread::scope(|threads| {
let own = threads.spawn(|| {
let _own = CacheScope::begin();
kept()
});
assert_eq!(own.join().unwrap(), Some(vec![]));
let shared = threads.spawn(|| {
let _shared = CacheScope::begin_shared(&handle);
keep(2);
kept()
});
assert_eq!(shared.join().unwrap(), Some(vec![1, 2]));
});
assert_eq!(kept(), Some(vec![1, 2]));
}
#[test]
fn scope_drop_order() {
let outer = CacheScope::begin();
let inner = CacheScope::begin();
keep(1);
drop(outer);
assert_eq!(kept(), Some(vec![1]), "the surviving scope keeps the cache");
drop(inner);
assert_eq!(kept(), None);
}
#[test]
fn cache_per_type() {
#[derive(Default)]
struct Other(Mutex<u32>);
let scope = CacheScope::begin();
keep(1);
*scope.handle().cache::<Other>().0.lock().unwrap() = 7;
assert_eq!(kept(), Some(vec![1]));
assert_eq!(*scope.handle().cache::<Other>().0.lock().unwrap(), 7);
}
pub(crate) type AdviceLog = Arc<Mutex<Vec<(Range<usize>, Advice)>>>;
struct Recording {
bytes: Vec<u8>,
log: AdviceLog,
mapping: bool,
}
impl sealed::Sealed for Recording {}
impl SharedSource for Recording {
fn bytes(&self) -> &[u8] {
&self.bytes
}
fn advise(&self, range: Range<usize>, advice: Advice) {
self.log.lock().unwrap().push((range, advice));
}
fn is_mapping(&self) -> bool {
self.mapping
}
}
pub(crate) fn recording_source(bytes: Vec<u8>, mapping: bool) -> (SharedBuffer, AdviceLog) {
let log = AdviceLog::default();
let source = Recording {
bytes,
log: Arc::clone(&log),
mapping,
};
(SharedBuffer::new(source), log)
}
#[test]
fn advise_translates_range() {
let (buffer, log) = recording_source(vec![7; 100], true);
let view = buffer.slice(10..50).expect("in range");
view.advise(5..100, Advice::WillNeed);
view.advise(60..70, Advice::Random);
view.advise(0..40, Advice::Normal);
assert_eq!(
*log.lock().unwrap(),
[(15..50, Advice::WillNeed), (10..50, Advice::Normal)],
"a range wholly past the view is dropped"
);
assert_eq!(&*view, [7; 40]);
}
#[test]
fn heap_ignores_advice() {
let buffer = SharedBuffer::new(vec![1, 2, 3]);
buffer.advise(0..3, Advice::WillNeed);
buffer.advise(0..3, Advice::Random);
assert!(!buffer.is_mapping());
assert_eq!(&*buffer, [1, 2, 3]);
}
}