feat(resource-viewer): добавить новый ресурсный просмотрщик с базовой функциональностью
feat(nres): улучшить структуру архива с добавлением заголовка и информации о записях feat(rsli): добавить поддержку заголовка библиотеки и улучшить обработку записей
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/*"]
|
members = ["crates/*", "apps/resource-viewer"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
11
apps/resource-viewer/Cargo.toml
Normal file
11
apps/resource-viewer/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "resource-viewer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
iced = "0.13"
|
||||||
|
rfd = "0.15"
|
||||||
|
nres = { path = "../../crates/nres" }
|
||||||
|
rsli = { path = "../../crates/rsli" }
|
||||||
518
apps/resource-viewer/src/main.rs
Normal file
518
apps/resource-viewer/src/main.rs
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
use iced::widget::{button, column, container, horizontal_space, row, scrollable, text};
|
||||||
|
use iced::{application, Element, Length, Task, Theme};
|
||||||
|
use rfd::FileDialog;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn main() -> iced::Result {
|
||||||
|
application("Parkan Resource Viewer", update, view)
|
||||||
|
.theme(theme)
|
||||||
|
.run_with(|| (ViewerApp::default(), Task::none()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme(_state: &ViewerApp) -> Theme {
|
||||||
|
Theme::Light
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ViewerApp {
|
||||||
|
document: Option<DocumentModel>,
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
OpenRequested,
|
||||||
|
SelectNode(Selection),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(state: &mut ViewerApp, message: Message) -> Task<Message> {
|
||||||
|
match message {
|
||||||
|
Message::OpenRequested => {
|
||||||
|
if let Some(path) = pick_archive_file() {
|
||||||
|
match load_document(&path) {
|
||||||
|
Ok(document) => {
|
||||||
|
state.status =
|
||||||
|
format!("Loaded {} as {}", path.display(), document.format.label());
|
||||||
|
state.document = Some(document);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
state.status = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::SelectNode(selection) => {
|
||||||
|
if let Some(document) = state.document.as_mut() {
|
||||||
|
document.selected = selection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(state: &ViewerApp) -> Element<'_, Message> {
|
||||||
|
let top_bar = row![
|
||||||
|
button("Open archive").on_press(Message::OpenRequested),
|
||||||
|
text(status_text(state)).size(14)
|
||||||
|
]
|
||||||
|
.spacing(12);
|
||||||
|
|
||||||
|
let content = if let Some(document) = &state.document {
|
||||||
|
view_document(document)
|
||||||
|
} else {
|
||||||
|
container(text("Open an .nres/.rsli/.lib archive to start.").size(16))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.center_y(Length::Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
container(column![top_bar, content].spacing(12).padding(12))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_text(state: &ViewerApp) -> String {
|
||||||
|
if state.status.is_empty() {
|
||||||
|
String::from("Ready")
|
||||||
|
} else {
|
||||||
|
state.status.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_document(document: &DocumentModel) -> Element<'_, Message> {
|
||||||
|
let mut tree = column![text("Archive tree").size(18)].spacing(6);
|
||||||
|
for item in &document.tree_rows {
|
||||||
|
let indent = horizontal_space().width(Length::Fixed(f32::from(item.depth) * 16.0));
|
||||||
|
|
||||||
|
let line = row![indent, text(&item.label).size(14)].spacing(6);
|
||||||
|
if let Some(selection) = item.selection {
|
||||||
|
let mut node_button = button(line)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.on_press(Message::SelectNode(selection));
|
||||||
|
|
||||||
|
if selection == document.selected {
|
||||||
|
node_button = node_button.style(button::primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = tree.push(node_button);
|
||||||
|
} else {
|
||||||
|
tree = tree.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (panel_title, fields) = selected_fields(document);
|
||||||
|
let mut fields_column = column![text(panel_title).size(18)].spacing(8);
|
||||||
|
|
||||||
|
for field in fields {
|
||||||
|
fields_column = fields_column.push(
|
||||||
|
row![
|
||||||
|
text(&field.key).size(14).width(Length::Fixed(220.0)),
|
||||||
|
text(&field.value).size(14).width(Length::Fill)
|
||||||
|
]
|
||||||
|
.spacing(12),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = container(scrollable(tree))
|
||||||
|
.width(Length::FillPortion(2))
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
let right = container(scrollable(fields_column))
|
||||||
|
.width(Length::FillPortion(5))
|
||||||
|
.height(Length::Fill);
|
||||||
|
|
||||||
|
row![left, right].spacing(12).height(Length::Fill).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_fields(document: &DocumentModel) -> (String, &[FieldRow]) {
|
||||||
|
match document.selected {
|
||||||
|
Selection::Archive => (
|
||||||
|
format!(
|
||||||
|
"{} fields ({})",
|
||||||
|
document.format.label(),
|
||||||
|
document.path.display()
|
||||||
|
),
|
||||||
|
&document.archive_fields,
|
||||||
|
),
|
||||||
|
Selection::Entry(index) => {
|
||||||
|
if let Some(entry) = document.entries.get(index) {
|
||||||
|
(entry.panel_title.clone(), &entry.fields)
|
||||||
|
} else {
|
||||||
|
(String::from("Entry"), &[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_archive_file() -> Option<PathBuf> {
|
||||||
|
FileDialog::new()
|
||||||
|
.set_title("Open Parkan archive")
|
||||||
|
.pick_file()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_document(path: &Path) -> Result<DocumentModel, String> {
|
||||||
|
let bytes =
|
||||||
|
fs::read(path).map_err(|err| format!("Failed to read {}: {err}", path.display()))?;
|
||||||
|
let Some(format) = detect_archive_format(&bytes) else {
|
||||||
|
return Err(format!(
|
||||||
|
"{} is not recognized as NRes/RsLi (unsupported magic).",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match format {
|
||||||
|
ArchiveFormat::Nres => load_nres_document(path),
|
||||||
|
ArchiveFormat::Rsli => load_rsli_document(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_archive_format(bytes: &[u8]) -> Option<ArchiveFormat> {
|
||||||
|
if bytes.len() >= 4 && &bytes[0..4] == b"NRes" {
|
||||||
|
return Some(ArchiveFormat::Nres);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.len() >= 2 && &bytes[0..2] == b"NL" {
|
||||||
|
return Some(ArchiveFormat::Rsli);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_nres_document(path: &Path) -> Result<DocumentModel, String> {
|
||||||
|
let archive = nres::Archive::open_path(path)
|
||||||
|
.map_err(|err| format!("NRes open failed for {}: {err}", path.display()))?;
|
||||||
|
|
||||||
|
let info = archive.info();
|
||||||
|
let mut archive_fields = vec![
|
||||||
|
FieldRow::new("format", "NRes"),
|
||||||
|
FieldRow::new("file_size", info.file_size.to_string()),
|
||||||
|
FieldRow::new("raw_mode", info.raw_mode.to_string()),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(header) = &info.header {
|
||||||
|
archive_fields.push(FieldRow::new(
|
||||||
|
"magic",
|
||||||
|
String::from_utf8_lossy(&header.magic).into_owned(),
|
||||||
|
));
|
||||||
|
archive_fields.push(FieldRow::new("version", format_u32_dec_hex(header.version)));
|
||||||
|
archive_fields.push(FieldRow::new("entry_count", header.entry_count.to_string()));
|
||||||
|
archive_fields.push(FieldRow::new(
|
||||||
|
"total_size",
|
||||||
|
format!("{} (0x{:08X})", header.total_size, header.total_size),
|
||||||
|
));
|
||||||
|
archive_fields.push(FieldRow::new(
|
||||||
|
"directory_offset",
|
||||||
|
header.directory_offset.to_string(),
|
||||||
|
));
|
||||||
|
archive_fields.push(FieldRow::new(
|
||||||
|
"directory_size",
|
||||||
|
header.directory_size.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for entry in archive.entries_inspect() {
|
||||||
|
let meta = entry.meta;
|
||||||
|
let mut fields = vec![
|
||||||
|
FieldRow::new("id", entry.id.0.to_string()),
|
||||||
|
FieldRow::new("name", meta.name.clone()),
|
||||||
|
FieldRow::new("type_id", format_u32_dec_hex(meta.kind)),
|
||||||
|
FieldRow::new("attr1", format_u32_dec_hex(meta.attr1)),
|
||||||
|
FieldRow::new("attr2", format_u32_dec_hex(meta.attr2)),
|
||||||
|
FieldRow::new("attr3", format_u32_dec_hex(meta.attr3)),
|
||||||
|
FieldRow::new("data_offset", meta.data_offset.to_string()),
|
||||||
|
FieldRow::new("data_size", meta.data_size.to_string()),
|
||||||
|
FieldRow::new("sort_index", meta.sort_index.to_string()),
|
||||||
|
FieldRow::new("name_raw_hex", bytes_as_hex(entry.name_raw)),
|
||||||
|
FieldRow::new("name_raw_ascii", bytes_as_ascii(entry.name_raw)),
|
||||||
|
];
|
||||||
|
|
||||||
|
fields.push(FieldRow::new("find_key", meta.name.to_ascii_lowercase()));
|
||||||
|
|
||||||
|
entries.push(EntryView {
|
||||||
|
full_name: meta.name.clone(),
|
||||||
|
panel_title: format!("NRes entry #{}: {}", entry.id.0, meta.name),
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree_rows = build_tree_rows(&entries);
|
||||||
|
|
||||||
|
Ok(DocumentModel {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
format: ArchiveFormat::Nres,
|
||||||
|
archive_fields,
|
||||||
|
entries,
|
||||||
|
tree_rows,
|
||||||
|
selected: Selection::Archive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_rsli_document(path: &Path) -> Result<DocumentModel, String> {
|
||||||
|
let library = rsli::Library::open_path(path)
|
||||||
|
.map_err(|err| format!("RsLi open failed for {}: {err}", path.display()))?;
|
||||||
|
|
||||||
|
let header = library.header();
|
||||||
|
let mut archive_fields = vec![
|
||||||
|
FieldRow::new("format", "RsLi"),
|
||||||
|
FieldRow::new("magic", String::from_utf8_lossy(&header.magic).into_owned()),
|
||||||
|
FieldRow::new(
|
||||||
|
"reserved",
|
||||||
|
format!("{} (0x{:02X})", header.reserved, header.reserved),
|
||||||
|
),
|
||||||
|
FieldRow::new(
|
||||||
|
"version",
|
||||||
|
format!("{} (0x{:02X})", header.version, header.version),
|
||||||
|
),
|
||||||
|
FieldRow::new("entry_count", header.entry_count.to_string()),
|
||||||
|
FieldRow::new("presorted_flag", format!("0x{:04X}", header.presorted_flag)),
|
||||||
|
FieldRow::new("xor_seed", format!("0x{:08X}", header.xor_seed)),
|
||||||
|
FieldRow::new("header_raw_hex", bytes_as_hex(&header.raw)),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(ao) = library.ao_trailer() {
|
||||||
|
archive_fields.push(FieldRow::new("ao_trailer", "present"));
|
||||||
|
archive_fields.push(FieldRow::new("ao_overlay", ao.overlay.to_string()));
|
||||||
|
archive_fields.push(FieldRow::new("ao_raw_hex", bytes_as_hex(&ao.raw)));
|
||||||
|
} else {
|
||||||
|
archive_fields.push(FieldRow::new("ao_trailer", "absent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for entry in library.entries_inspect() {
|
||||||
|
let meta = entry.meta;
|
||||||
|
let method_raw = (meta.flags as u16 as u32) & 0x1E0;
|
||||||
|
|
||||||
|
let fields = vec![
|
||||||
|
FieldRow::new("id", entry.id.0.to_string()),
|
||||||
|
FieldRow::new("name", meta.name.clone()),
|
||||||
|
FieldRow::new(
|
||||||
|
"flags",
|
||||||
|
format!("{} (0x{:04X})", meta.flags, meta.flags as u16),
|
||||||
|
),
|
||||||
|
FieldRow::new("method", format!("{:?}", meta.method)),
|
||||||
|
FieldRow::new("method_raw", format!("0x{:03X}", method_raw)),
|
||||||
|
FieldRow::new("packed_size", meta.packed_size.to_string()),
|
||||||
|
FieldRow::new("unpacked_size", meta.unpacked_size.to_string()),
|
||||||
|
FieldRow::new("data_offset_effective", meta.data_offset.to_string()),
|
||||||
|
FieldRow::new("data_offset_raw", entry.data_offset_raw.to_string()),
|
||||||
|
FieldRow::new("sort_to_original", entry.sort_to_original.to_string()),
|
||||||
|
FieldRow::new("name_raw_hex", bytes_as_hex(entry.name_raw)),
|
||||||
|
FieldRow::new("name_raw_ascii", bytes_as_ascii(entry.name_raw)),
|
||||||
|
FieldRow::new("service_tail_hex", bytes_as_hex(entry.service_tail)),
|
||||||
|
FieldRow::new("service_tail_ascii", bytes_as_ascii(entry.service_tail)),
|
||||||
|
];
|
||||||
|
|
||||||
|
entries.push(EntryView {
|
||||||
|
full_name: meta.name.clone(),
|
||||||
|
panel_title: format!("RsLi entry #{}: {}", entry.id.0, meta.name),
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree_rows = build_tree_rows(&entries);
|
||||||
|
|
||||||
|
Ok(DocumentModel {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
format: ArchiveFormat::Rsli,
|
||||||
|
archive_fields,
|
||||||
|
entries,
|
||||||
|
tree_rows,
|
||||||
|
selected: Selection::Archive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_tree_rows(entries: &[EntryView]) -> Vec<TreeRow> {
|
||||||
|
let mut root = FolderNode::default();
|
||||||
|
for (index, entry) in entries.iter().enumerate() {
|
||||||
|
insert_tree_path(&mut root, &entry.full_name, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rows = vec![TreeRow {
|
||||||
|
depth: 0,
|
||||||
|
label: String::from("[Archive fields]"),
|
||||||
|
selection: Some(Selection::Archive),
|
||||||
|
}];
|
||||||
|
|
||||||
|
flatten_tree(&root, 0, &mut rows);
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_tree_path(root: &mut FolderNode, full_name: &str, entry_index: usize) {
|
||||||
|
let mut parts: Vec<&str> = full_name
|
||||||
|
.split(['/', '\\'])
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
parts.push(full_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.len() == 1 {
|
||||||
|
root.files.push((parts[0].to_string(), entry_index));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = parts.pop().unwrap_or(full_name);
|
||||||
|
let mut node = root;
|
||||||
|
for part in parts {
|
||||||
|
node = node.folders.entry(part.to_string()).or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
node.files.push((file_name.to_string(), entry_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_tree(node: &FolderNode, depth: u16, out: &mut Vec<TreeRow>) {
|
||||||
|
for (folder_name, folder_node) in &node.folders {
|
||||||
|
out.push(TreeRow {
|
||||||
|
depth,
|
||||||
|
label: format!("{folder_name}/"),
|
||||||
|
selection: None,
|
||||||
|
});
|
||||||
|
flatten_tree(folder_node, depth.saturating_add(1), out);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut files = node.files.clone();
|
||||||
|
files.sort_by(|left, right| left.0.cmp(&right.0));
|
||||||
|
|
||||||
|
for (name, index) in files {
|
||||||
|
out.push(TreeRow {
|
||||||
|
depth,
|
||||||
|
label: name,
|
||||||
|
selection: Some(Selection::Entry(index)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_as_hex(bytes: &[u8]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for (index, byte) in bytes.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
let _ = write!(&mut out, "{byte:02X}");
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_as_ascii(bytes: &[u8]) -> String {
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|byte| {
|
||||||
|
if byte.is_ascii_graphic() || *byte == b' ' {
|
||||||
|
char::from(*byte)
|
||||||
|
} else {
|
||||||
|
'.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_u32_dec_hex(value: u32) -> String {
|
||||||
|
format!("{} (0x{:08X})", value, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct DocumentModel {
|
||||||
|
path: PathBuf,
|
||||||
|
format: ArchiveFormat,
|
||||||
|
archive_fields: Vec<FieldRow>,
|
||||||
|
entries: Vec<EntryView>,
|
||||||
|
tree_rows: Vec<TreeRow>,
|
||||||
|
selected: Selection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum ArchiveFormat {
|
||||||
|
Nres,
|
||||||
|
Rsli,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArchiveFormat {
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Nres => "NRes",
|
||||||
|
Self::Rsli => "RsLi",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct EntryView {
|
||||||
|
full_name: String,
|
||||||
|
panel_title: String,
|
||||||
|
fields: Vec<FieldRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct FieldRow {
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldRow {
|
||||||
|
fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
key: key.into(),
|
||||||
|
value: value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TreeRow {
|
||||||
|
depth: u16,
|
||||||
|
label: String,
|
||||||
|
selection: Option<Selection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Selection {
|
||||||
|
Archive,
|
||||||
|
Entry(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct FolderNode {
|
||||||
|
folders: BTreeMap<String, FolderNode>,
|
||||||
|
files: Vec<(String, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tree_builds_nested_paths() {
|
||||||
|
let entries = vec![
|
||||||
|
EntryView {
|
||||||
|
full_name: String::from("textures/ui/hud.texm"),
|
||||||
|
panel_title: String::new(),
|
||||||
|
fields: vec![],
|
||||||
|
},
|
||||||
|
EntryView {
|
||||||
|
full_name: String::from("textures/world/ground.texm"),
|
||||||
|
panel_title: String::new(),
|
||||||
|
fields: vec![],
|
||||||
|
},
|
||||||
|
EntryView {
|
||||||
|
full_name: String::from("root_file.msh"),
|
||||||
|
panel_title: String::new(),
|
||||||
|
fields: vec![],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let rows = build_tree_rows(&entries);
|
||||||
|
assert!(rows.iter().any(|row| row.label == "textures/"));
|
||||||
|
assert!(rows.iter().any(|row| row.label == "ui/"));
|
||||||
|
assert!(rows.iter().any(|row| row.label == "hud.texm"));
|
||||||
|
assert!(rows.iter().any(|row| row.label == "root_file.msh"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,10 +26,28 @@ pub enum OpenMode {
|
|||||||
ReadWrite,
|
ReadWrite,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ArchiveHeader {
|
||||||
|
pub magic: [u8; 4],
|
||||||
|
pub version: u32,
|
||||||
|
pub entry_count: u32,
|
||||||
|
pub total_size: u32,
|
||||||
|
pub directory_offset: u64,
|
||||||
|
pub directory_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ArchiveInfo {
|
||||||
|
pub raw_mode: bool,
|
||||||
|
pub file_size: u64,
|
||||||
|
pub header: Option<ArchiveHeader>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Archive {
|
pub struct Archive {
|
||||||
bytes: Arc<[u8]>,
|
bytes: Arc<[u8]>,
|
||||||
entries: Vec<EntryRecord>,
|
entries: Vec<EntryRecord>,
|
||||||
|
info: ArchiveInfo,
|
||||||
raw_mode: bool,
|
raw_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +72,13 @@ pub struct EntryRef<'a> {
|
|||||||
pub meta: &'a EntryMeta,
|
pub meta: &'a EntryMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct EntryInspect<'a> {
|
||||||
|
pub id: EntryId,
|
||||||
|
pub meta: &'a EntryMeta,
|
||||||
|
pub name_raw: &'a [u8; 36],
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct EntryRecord {
|
struct EntryRecord {
|
||||||
meta: EntryMeta,
|
meta: EntryMeta,
|
||||||
@@ -76,17 +101,27 @@ impl Archive {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_bytes(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Self> {
|
pub fn open_bytes(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Self> {
|
||||||
let (entries, _) = parse_archive(&bytes, opts.raw_mode)?;
|
let file_size = u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?;
|
||||||
|
let (entries, header) = parse_archive(&bytes, opts.raw_mode)?;
|
||||||
if opts.prefetch_pages {
|
if opts.prefetch_pages {
|
||||||
prefetch_pages(&bytes);
|
prefetch_pages(&bytes);
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
bytes,
|
bytes,
|
||||||
entries,
|
entries,
|
||||||
|
info: ArchiveInfo {
|
||||||
|
raw_mode: opts.raw_mode,
|
||||||
|
file_size,
|
||||||
|
header,
|
||||||
|
},
|
||||||
raw_mode: opts.raw_mode,
|
raw_mode: opts.raw_mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn info(&self) -> &ArchiveInfo {
|
||||||
|
&self.info
|
||||||
|
}
|
||||||
|
|
||||||
pub fn entry_count(&self) -> usize {
|
pub fn entry_count(&self) -> usize {
|
||||||
self.entries.len()
|
self.entries.len()
|
||||||
}
|
}
|
||||||
@@ -101,6 +136,17 @@ impl Archive {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn entries_inspect(&self) -> impl Iterator<Item = EntryInspect<'_>> {
|
||||||
|
self.entries.iter().enumerate().filter_map(|(idx, entry)| {
|
||||||
|
let id = u32::try_from(idx).ok()?;
|
||||||
|
Some(EntryInspect {
|
||||||
|
id: EntryId(id),
|
||||||
|
meta: &entry.meta,
|
||||||
|
name_raw: &entry.name_raw,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find(&self, name: &str) -> Option<EntryId> {
|
pub fn find(&self, name: &str) -> Option<EntryId> {
|
||||||
if self.entries.is_empty() {
|
if self.entries.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -153,6 +199,16 @@ impl Archive {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inspect(&self, id: EntryId) -> Option<EntryInspect<'_>> {
|
||||||
|
let idx = usize::try_from(id.0).ok()?;
|
||||||
|
let entry = self.entries.get(idx)?;
|
||||||
|
Some(EntryInspect {
|
||||||
|
id,
|
||||||
|
meta: &entry.meta,
|
||||||
|
name_raw: &entry.name_raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(&self, id: EntryId) -> Result<ResourceData<'_>> {
|
pub fn read(&self, id: EntryId) -> Result<ResourceData<'_>> {
|
||||||
let range = self.entry_range(id)?;
|
let range = self.entry_range(id)?;
|
||||||
Ok(ResourceData::Borrowed(&self.bytes[range]))
|
Ok(ResourceData::Borrowed(&self.bytes[range]))
|
||||||
@@ -377,7 +433,10 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_archive(bytes: &[u8], raw_mode: bool) -> Result<(Vec<EntryRecord>, u64)> {
|
fn parse_archive(
|
||||||
|
bytes: &[u8],
|
||||||
|
raw_mode: bool,
|
||||||
|
) -> Result<(Vec<EntryRecord>, Option<ArchiveHeader>)> {
|
||||||
if raw_mode {
|
if raw_mode {
|
||||||
let data_size = u32::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?;
|
let data_size = u32::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?;
|
||||||
let entry = EntryRecord {
|
let entry = EntryRecord {
|
||||||
@@ -398,10 +457,7 @@ fn parse_archive(bytes: &[u8], raw_mode: bool) -> Result<(Vec<EntryRecord>, u64)
|
|||||||
name
|
name
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return Ok((
|
return Ok((vec![entry], None));
|
||||||
vec![entry],
|
|
||||||
u64::try_from(bytes.len()).map_err(|_| Error::IntegerOverflow)?,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.len() < 16 {
|
if bytes.len() < 16 {
|
||||||
@@ -526,7 +582,17 @@ fn parse_archive(bytes: &[u8], raw_mode: bool) -> Result<(Vec<EntryRecord>, u64)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((entries, directory_offset))
|
Ok((
|
||||||
|
entries,
|
||||||
|
Some(ArchiveHeader {
|
||||||
|
magic: *b"NRes",
|
||||||
|
version,
|
||||||
|
entry_count: u32::try_from(entry_count).map_err(|_| Error::IntegerOverflow)?,
|
||||||
|
total_size,
|
||||||
|
directory_offset,
|
||||||
|
directory_size: directory_len,
|
||||||
|
}),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checked_range(offset: u64, size: u32, bytes_len: usize) -> Result<Range<usize>> {
|
fn checked_range(offset: u64, size: u32, bytes_len: usize) -> Result<Range<usize>> {
|
||||||
|
|||||||
@@ -30,20 +30,33 @@ impl Default for OpenOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LibraryHeader {
|
||||||
|
pub raw: [u8; 32],
|
||||||
|
pub magic: [u8; 2],
|
||||||
|
pub reserved: u8,
|
||||||
|
pub version: u8,
|
||||||
|
pub entry_count: i16,
|
||||||
|
pub presorted_flag: u16,
|
||||||
|
pub xor_seed: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AoTrailer {
|
||||||
|
pub raw: [u8; 6],
|
||||||
|
pub overlay: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Library {
|
pub struct Library {
|
||||||
bytes: Arc<[u8]>,
|
bytes: Arc<[u8]>,
|
||||||
entries: Vec<EntryRecord>,
|
entries: Vec<EntryRecord>,
|
||||||
#[cfg(test)]
|
header: LibraryHeader,
|
||||||
pub(crate) header_raw: [u8; 32],
|
ao_trailer: Option<AoTrailer>,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) table_plain_original: Vec<u8>,
|
pub(crate) table_plain_original: Vec<u8>,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) xor_seed: u32,
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) source_size: usize,
|
pub(crate) source_size: usize,
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) trailer_raw: Option<[u8; 6]>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
@@ -77,6 +90,16 @@ pub struct EntryRef<'a> {
|
|||||||
pub meta: &'a EntryMeta,
|
pub meta: &'a EntryMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct EntryInspect<'a> {
|
||||||
|
pub id: EntryId,
|
||||||
|
pub meta: &'a EntryMeta,
|
||||||
|
pub name_raw: &'a [u8; 12],
|
||||||
|
pub service_tail: &'a [u8; 4],
|
||||||
|
pub sort_to_original: i16,
|
||||||
|
pub data_offset_raw: u32,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PackedResource {
|
pub struct PackedResource {
|
||||||
pub meta: EntryMeta,
|
pub meta: EntryMeta,
|
||||||
pub packed: Vec<u8>,
|
pub packed: Vec<u8>,
|
||||||
@@ -86,9 +109,9 @@ pub struct PackedResource {
|
|||||||
pub(crate) struct EntryRecord {
|
pub(crate) struct EntryRecord {
|
||||||
pub(crate) meta: EntryMeta,
|
pub(crate) meta: EntryMeta,
|
||||||
pub(crate) name_raw: [u8; 12],
|
pub(crate) name_raw: [u8; 12],
|
||||||
|
pub(crate) service_tail: [u8; 4],
|
||||||
pub(crate) sort_to_original: i16,
|
pub(crate) sort_to_original: i16,
|
||||||
pub(crate) key16: u16,
|
pub(crate) key16: u16,
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) data_offset_raw: u32,
|
pub(crate) data_offset_raw: u32,
|
||||||
pub(crate) packed_size_declared: u32,
|
pub(crate) packed_size_declared: u32,
|
||||||
pub(crate) packed_size_available: usize,
|
pub(crate) packed_size_available: usize,
|
||||||
@@ -106,6 +129,14 @@ impl Library {
|
|||||||
parse_library(arc, opts)
|
parse_library(arc, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn header(&self) -> &LibraryHeader {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ao_trailer(&self) -> Option<&AoTrailer> {
|
||||||
|
self.ao_trailer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn entry_count(&self) -> usize {
|
pub fn entry_count(&self) -> usize {
|
||||||
self.entries.len()
|
self.entries.len()
|
||||||
}
|
}
|
||||||
@@ -120,6 +151,20 @@ impl Library {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn entries_inspect(&self) -> impl Iterator<Item = EntryInspect<'_>> {
|
||||||
|
self.entries.iter().enumerate().filter_map(|(idx, entry)| {
|
||||||
|
let id = u32::try_from(idx).ok()?;
|
||||||
|
Some(EntryInspect {
|
||||||
|
id: EntryId(id),
|
||||||
|
meta: &entry.meta,
|
||||||
|
name_raw: &entry.name_raw,
|
||||||
|
service_tail: &entry.service_tail,
|
||||||
|
sort_to_original: entry.sort_to_original,
|
||||||
|
data_offset_raw: entry.data_offset_raw,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find(&self, name: &str) -> Option<EntryId> {
|
pub fn find(&self, name: &str) -> Option<EntryId> {
|
||||||
if self.entries.is_empty() {
|
if self.entries.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -187,6 +232,19 @@ impl Library {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inspect(&self, id: EntryId) -> Option<EntryInspect<'_>> {
|
||||||
|
let idx = usize::try_from(id.0).ok()?;
|
||||||
|
let entry = self.entries.get(idx)?;
|
||||||
|
Some(EntryInspect {
|
||||||
|
id,
|
||||||
|
meta: &entry.meta,
|
||||||
|
name_raw: &entry.name_raw,
|
||||||
|
service_tail: &entry.service_tail,
|
||||||
|
sort_to_original: entry.sort_to_original,
|
||||||
|
data_offset_raw: entry.data_offset_raw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(&self, id: EntryId) -> Result<Vec<u8>> {
|
pub fn load(&self, id: EntryId) -> Result<Vec<u8>> {
|
||||||
let entry = self.entry_by_id(id)?;
|
let entry = self.entry_by_id(id)?;
|
||||||
let packed = self.packed_slice(id, entry)?;
|
let packed = self.packed_slice(id, entry)?;
|
||||||
@@ -284,7 +342,7 @@ impl Library {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn rebuild_from_parsed_metadata(&self) -> Result<Vec<u8>> {
|
pub(crate) fn rebuild_from_parsed_metadata(&self) -> Result<Vec<u8>> {
|
||||||
let trailer_len = usize::from(self.trailer_raw.is_some()) * 6;
|
let trailer_len = usize::from(self.ao_trailer.is_some()) * 6;
|
||||||
let pre_trailer_size = self
|
let pre_trailer_size = self
|
||||||
.source_size
|
.source_size
|
||||||
.checked_sub(trailer_len)
|
.checked_sub(trailer_len)
|
||||||
@@ -304,9 +362,11 @@ impl Library {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut out = vec![0u8; pre_trailer_size];
|
let mut out = vec![0u8; pre_trailer_size];
|
||||||
out[0..32].copy_from_slice(&self.header_raw);
|
out[0..32].copy_from_slice(&self.header.raw);
|
||||||
let encrypted_table =
|
let encrypted_table = xor_stream(
|
||||||
xor_stream(&self.table_plain_original, (self.xor_seed & 0xFFFF) as u16);
|
&self.table_plain_original,
|
||||||
|
(self.header.xor_seed & 0xFFFF) as u16,
|
||||||
|
);
|
||||||
out[32..table_end].copy_from_slice(&encrypted_table);
|
out[32..table_end].copy_from_slice(&encrypted_table);
|
||||||
|
|
||||||
let mut occupied = vec![false; pre_trailer_size];
|
let mut occupied = vec![false; pre_trailer_size];
|
||||||
@@ -337,8 +397,8 @@ impl Library {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(trailer) = self.trailer_raw {
|
if let Some(trailer) = &self.ao_trailer {
|
||||||
out.extend_from_slice(&trailer);
|
out.extend_from_slice(&trailer.raw);
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::compress::xor::xor_stream;
|
use crate::compress::xor::xor_stream;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::{EntryMeta, EntryRecord, Library, OpenOptions, PackMethod, Result};
|
use crate::{
|
||||||
|
AoTrailer, EntryMeta, EntryRecord, Library, LibraryHeader, OpenOptions, PackMethod, Result,
|
||||||
|
};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -16,13 +18,17 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
let mut header_raw = [0u8; 32];
|
let mut header_raw = [0u8; 32];
|
||||||
header_raw.copy_from_slice(&bytes[0..32]);
|
header_raw.copy_from_slice(&bytes[0..32]);
|
||||||
|
|
||||||
if &bytes[0..2] != b"NL" {
|
let mut magic = [0u8; 2];
|
||||||
|
magic.copy_from_slice(&bytes[0..2]);
|
||||||
|
if &magic != b"NL" {
|
||||||
let mut got = [0u8; 2];
|
let mut got = [0u8; 2];
|
||||||
got.copy_from_slice(&bytes[0..2]);
|
got.copy_from_slice(&bytes[0..2]);
|
||||||
return Err(Error::InvalidMagic { got });
|
return Err(Error::InvalidMagic { got });
|
||||||
}
|
}
|
||||||
if bytes[3] != 0x01 {
|
let reserved = bytes[2];
|
||||||
return Err(Error::UnsupportedVersion { got: bytes[3] });
|
let version = bytes[3];
|
||||||
|
if version != 0x01 {
|
||||||
|
return Err(Error::UnsupportedVersion { got: version });
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry_count = i16::from_le_bytes([bytes[4], bytes[5]]);
|
let entry_count = i16::from_le_bytes([bytes[4], bytes[5]]);
|
||||||
@@ -36,7 +42,17 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
return Err(Error::TooManyEntries { got: count });
|
return Err(Error::TooManyEntries { got: count });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let presorted_flag = u16::from_le_bytes([bytes[14], bytes[15]]);
|
||||||
let xor_seed = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
|
let xor_seed = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
|
||||||
|
let header = LibraryHeader {
|
||||||
|
raw: header_raw,
|
||||||
|
magic,
|
||||||
|
reserved,
|
||||||
|
version,
|
||||||
|
entry_count,
|
||||||
|
presorted_flag,
|
||||||
|
xor_seed,
|
||||||
|
};
|
||||||
|
|
||||||
let table_len = count.checked_mul(32).ok_or(Error::IntegerOverflow)?;
|
let table_len = count.checked_mul(32).ok_or(Error::IntegerOverflow)?;
|
||||||
let table_offset = 32usize;
|
let table_offset = 32usize;
|
||||||
@@ -58,8 +74,6 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (overlay, trailer_raw) = parse_ao_trailer(&bytes, opts.allow_ao_trailer)?;
|
let (overlay, trailer_raw) = parse_ao_trailer(&bytes, opts.allow_ao_trailer)?;
|
||||||
#[cfg(not(test))]
|
|
||||||
let _ = trailer_raw;
|
|
||||||
|
|
||||||
let mut entries = Vec::with_capacity(count);
|
let mut entries = Vec::with_capacity(count);
|
||||||
for idx in 0..count {
|
for idx in 0..count {
|
||||||
@@ -67,6 +81,8 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
|
|
||||||
let mut name_raw = [0u8; 12];
|
let mut name_raw = [0u8; 12];
|
||||||
name_raw.copy_from_slice(&row[0..12]);
|
name_raw.copy_from_slice(&row[0..12]);
|
||||||
|
let mut service_tail = [0u8; 4];
|
||||||
|
service_tail.copy_from_slice(&row[12..16]);
|
||||||
|
|
||||||
let flags_signed = i16::from_le_bytes([row[16], row[17]]);
|
let flags_signed = i16::from_le_bytes([row[16], row[17]]);
|
||||||
let sort_to_original = i16::from_le_bytes([row[18], row[19]]);
|
let sort_to_original = i16::from_le_bytes([row[18], row[19]]);
|
||||||
@@ -137,9 +153,9 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
unpacked_size,
|
unpacked_size,
|
||||||
},
|
},
|
||||||
name_raw,
|
name_raw,
|
||||||
|
service_tail,
|
||||||
sort_to_original,
|
sort_to_original,
|
||||||
key16: sort_to_original as u16,
|
key16: sort_to_original as u16,
|
||||||
#[cfg(test)]
|
|
||||||
data_offset_raw,
|
data_offset_raw,
|
||||||
packed_size_declared,
|
packed_size_declared,
|
||||||
packed_size_available,
|
packed_size_available,
|
||||||
@@ -147,7 +163,6 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let presorted_flag = u16::from_le_bytes([bytes[14], bytes[15]]);
|
|
||||||
if presorted_flag == 0xABBA {
|
if presorted_flag == 0xABBA {
|
||||||
let mut seen = vec![false; count];
|
let mut seen = vec![false; count];
|
||||||
for entry in &entries {
|
for entry in &entries {
|
||||||
@@ -196,16 +211,12 @@ pub fn parse_library(bytes: Arc<[u8]>, opts: OpenOptions) -> Result<Library> {
|
|||||||
Ok(Library {
|
Ok(Library {
|
||||||
bytes,
|
bytes,
|
||||||
entries,
|
entries,
|
||||||
#[cfg(test)]
|
header,
|
||||||
header_raw,
|
ao_trailer: trailer_raw.map(|raw| AoTrailer { raw, overlay }),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
table_plain_original,
|
table_plain_original,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
xor_seed,
|
|
||||||
#[cfg(test)]
|
|
||||||
source_size,
|
source_size,
|
||||||
#[cfg(test)]
|
|
||||||
trailer_raw,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user