diff --git a/crates/fparkan-nres/src/lib.rs b/crates/fparkan-nres/src/lib.rs index 778b76b..e82ca08 100644 --- a/crates/fparkan-nres/src/lib.rs +++ b/crates/fparkan-nres/src/lib.rs @@ -479,6 +479,23 @@ impl NresDocument { Ok(&self.bytes[entry.data_range.clone()]) } + /// Returns an owned view of an entry payload without copying its bytes. + /// + /// The returned owner is the immutable archive buffer. Consumers may retain + /// the view after dropping this document. + /// + /// # Errors + /// + /// Returns [`NresError::EntryIdOutOfRange`] when `id` is not present in + /// this document. + pub fn payload_view(&self, id: EntryId) -> Result<(Arc<[u8]>, Range), NresError> { + let entry = self.entry(id).ok_or_else(|| NresError::EntryIdOutOfRange { + id: id.0, + entry_count: saturating_u32_len(self.entries.len()), + })?; + Ok((Arc::clone(&self.bytes), entry.data_range.clone())) + } + /// Encodes the document according to the selected write profile. #[must_use] pub fn encode(&self, profile: WriteProfile) -> Vec { @@ -1213,6 +1230,8 @@ mod tests { assert_eq!(entry.data_range().end, HEADER_LEN + 1); assert_eq!(doc.header().directory_offset % 8, 0); assert_eq!(doc.payload(EntryId(0)).expect("payload"), b"x"); + let (owner, range) = doc.payload_view(EntryId(0)).expect("payload view"); + assert_eq!(&owner[range], b"x"); } #[test] diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs index 96be4d8..8fbd62e 100644 --- a/crates/fparkan-resource/src/lib.rs +++ b/crates/fparkan-resource/src/lib.rs @@ -381,7 +381,7 @@ struct DecodedPayloadCache { #[derive(Clone, Debug)] struct PayloadCacheEntry { - bytes: Arc<[u8]>, + bytes: ResourceBytes, last_access: u64, } @@ -562,7 +562,7 @@ impl ResourceRepository for CachedResourceRepository { let task = { let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?; if let Some(bytes) = state.payload_cache.get(entry) { - return Ok(ResourceBytes::Shared(bytes)); + return Ok(bytes); } state.payload_decode_task(entry)? }; @@ -573,15 +573,14 @@ impl ResourceRepository for CachedResourceRepository { key: task.key, source, })?; - let shared = Arc::from(payload.into_boxed_slice()); let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?; if let Some(bytes) = state.payload_cache.get(entry) { - return Ok(ResourceBytes::Shared(bytes)); + return Ok(bytes); } state.entry_archive(entry)?; - state.payload_cache.insert(entry, Arc::clone(&shared)); - Ok(ResourceBytes::Shared(shared)) + state.payload_cache.insert(entry, payload.clone()); + Ok(payload) } fn entry_info(&self, entry: EntryHandle) -> Result { @@ -638,14 +637,14 @@ impl DecodedPayloadCache { } } - fn get(&mut self, handle: EntryHandle) -> Option> { + fn get(&mut self, handle: EntryHandle) -> Option { let entry = self.entries.get_mut(&handle)?; self.generation = self.generation.saturating_add(1); entry.last_access = self.generation; - Some(Arc::clone(&entry.bytes)) + Some(entry.bytes.clone()) } - fn insert(&mut self, handle: EntryHandle, bytes: Arc<[u8]>) { + fn insert(&mut self, handle: EntryHandle, bytes: ResourceBytes) { let len = bytes.len(); if self.max_entries == 0 || len > self.max_bytes { return; @@ -815,14 +814,15 @@ impl ArchiveSlot { } impl ArchiveDocument { - fn read_payload(&self, local: u32) -> Result, String> { + fn read_payload(&self, local: u32) -> Result { match self { ArchiveDocument::Nres(document) => document - .payload(fparkan_nres::EntryId(local)) - .map(<[u8]>::to_vec) + .payload_view(fparkan_nres::EntryId(local)) + .map(|(owner, range)| ResourceBytes::Slice { owner, range }) .map_err(|err| err.to_string()), ArchiveDocument::Rsli(document) => document .load(fparkan_rsli::EntryId(local)) + .map(|payload| ResourceBytes::Shared(Arc::from(payload.into_boxed_slice()))) .map_err(|err| err.to_string()), } } @@ -1070,7 +1070,9 @@ mod tests { .find(first, &resource_name(b"alpha.txt")) .expect("find") .expect("entry"); - assert_eq!(repo.read(handle).expect("read").as_slice(), b"alpha"); + let payload = repo.read(handle).expect("read"); + assert_eq!(payload.as_slice(), b"alpha"); + assert!(matches!(payload, ResourceBytes::Slice { .. })); let info = repo.entry_info(handle).expect("entry info"); assert_eq!(info.key.archive, path); assert!(info.key.name.0.eq_ignore_ascii_case(b"Alpha.TXT")); diff --git a/docs/tomes/03-resources.md b/docs/tomes/03-resources.md index acf97a8..7bce1a1 100644 --- a/docs/tomes/03-resources.md +++ b/docs/tomes/03-resources.md @@ -128,6 +128,12 @@ directory_offset = total_size - directory_size Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а каталог заканчивается точно на `total_size`. +В repository NRes payload возвращается как slice с `Arc`-владельцем полного +неизменяемого архива: валидация формата не создаёт промежуточную копию каждого +payload. Такой view учитывается в том же bounded decoded-payload cache по +длине диапазона и сохраняет deterministic eviction. Это применимо только к +raw NRes; сжатый RsLi по-прежнему материализуется после декодирования. + ### Запись каталога NRes ```c