|
|
@@ -259,8 +259,17 @@ pub fn inspect_model_from_root(
|
|
|
|
archive: &str,
|
|
|
|
archive: &str,
|
|
|
|
resource: &str,
|
|
|
|
resource: &str,
|
|
|
|
) -> Result<ModelInspection, String> {
|
|
|
|
) -> Result<ModelInspection, String> {
|
|
|
|
let bytes = read_resource_bytes(root, archive, resource)?;
|
|
|
|
let bytes =
|
|
|
|
let document = decode_nres(bytes, ReadProfile::Compatible).map_err(|err| err.to_string())?;
|
|
|
|
read_resource_bytes_diagnostic(root, archive, resource).map_err(|err| render_human(&err))?;
|
|
|
|
|
|
|
|
let document = decode_nres(bytes.clone(), ReadProfile::Compatible).map_err(|err| {
|
|
|
|
|
|
|
|
render_human(&resource_parse_diagnostic(
|
|
|
|
|
|
|
|
"S1.NRES.DECODE",
|
|
|
|
|
|
|
|
archive,
|
|
|
|
|
|
|
|
resource,
|
|
|
|
|
|
|
|
&bytes,
|
|
|
|
|
|
|
|
err.to_string(),
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
})?;
|
|
|
|
let msh = decode_msh(&document).map_err(|err| err.to_string())?;
|
|
|
|
let msh = decode_msh(&document).map_err(|err| err.to_string())?;
|
|
|
|
let validated = validate_msh(&msh).map_err(|err| err.to_string())?;
|
|
|
|
let validated = validate_msh(&msh).map_err(|err| err.to_string())?;
|
|
|
|
Ok(ModelInspection {
|
|
|
|
Ok(ModelInspection {
|
|
|
@@ -284,7 +293,10 @@ pub fn load_model_from_root(
|
|
|
|
archive: &str,
|
|
|
|
archive: &str,
|
|
|
|
resource: &str,
|
|
|
|
resource: &str,
|
|
|
|
) -> Result<ModelAsset, String> {
|
|
|
|
) -> Result<ModelAsset, String> {
|
|
|
|
let document = load_model_document_from_root(root, archive, resource)?;
|
|
|
|
let document =
|
|
|
|
|
|
|
|
load_model_document_from_root_diagnostic(root, archive, resource).map_err(|err| {
|
|
|
|
|
|
|
|
render_human(&err)
|
|
|
|
|
|
|
|
})?;
|
|
|
|
let msh = decode_msh(&document).map_err(|err| err.to_string())?;
|
|
|
|
let msh = decode_msh(&document).map_err(|err| err.to_string())?;
|
|
|
|
validate_msh(&msh).map_err(|err| err.to_string())
|
|
|
|
validate_msh(&msh).map_err(|err| err.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
@@ -300,7 +312,8 @@ pub fn inspect_texture_from_root(
|
|
|
|
archive: &str,
|
|
|
|
archive: &str,
|
|
|
|
resource: &str,
|
|
|
|
resource: &str,
|
|
|
|
) -> Result<TextureInspection, String> {
|
|
|
|
) -> Result<TextureInspection, String> {
|
|
|
|
let bytes = read_resource_bytes(root, archive, resource)?;
|
|
|
|
let bytes =
|
|
|
|
|
|
|
|
read_resource_bytes_diagnostic(root, archive, resource).map_err(|err| render_human(&err))?;
|
|
|
|
let document = decode_texm(bytes).map_err(|err| err.to_string())?;
|
|
|
|
let document = decode_texm(bytes).map_err(|err| err.to_string())?;
|
|
|
|
Ok(TextureInspection {
|
|
|
|
Ok(TextureInspection {
|
|
|
|
width: document.width(),
|
|
|
|
width: document.width(),
|
|
|
@@ -355,33 +368,86 @@ fn inspect_land_map(document: &NresDocument) -> Result<MapInspection, String> {
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn read_resource_bytes(root: &Path, archive: &str, name: &str) -> Result<Arc<[u8]>, String> {
|
|
|
|
fn read_resource_bytes_diagnostic(
|
|
|
|
|
|
|
|
root: &Path,
|
|
|
|
|
|
|
|
archive: &str,
|
|
|
|
|
|
|
|
name: &str,
|
|
|
|
|
|
|
|
) -> Result<Arc<[u8]>, Diagnostic> {
|
|
|
|
let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
|
|
|
|
let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
|
|
|
|
let archive_path = archive_path(archive.as_bytes()).map_err(|err| err.to_string())?;
|
|
|
|
let archive_path = archive_path(archive.as_bytes()).map_err(|err| {
|
|
|
|
|
|
|
|
diagnostic(DiagnosticCode("S1.PATH.ARCHIVE"), err.to_string()).with_context(
|
|
|
|
|
|
|
|
DiagnosticContext {
|
|
|
|
|
|
|
|
phase: Some(Phase::Resolve),
|
|
|
|
|
|
|
|
path: Some(archive.to_string()),
|
|
|
|
|
|
|
|
archive_entry: Some(name.to_string()),
|
|
|
|
|
|
|
|
..DiagnosticContext::default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
})?;
|
|
|
|
let resource_name = resource_name(name.as_bytes());
|
|
|
|
let resource_name = resource_name(name.as_bytes());
|
|
|
|
let archive_handle = repository
|
|
|
|
let archive_handle = repository
|
|
|
|
.open_archive(&archive_path)
|
|
|
|
.open_archive(&archive_path)
|
|
|
|
.map_err(|err| format!("{err}"))?;
|
|
|
|
.map_err(|err| {
|
|
|
|
|
|
|
|
diagnostic(DiagnosticCode("S1.RESOURCE.OPEN_ARCHIVE"), err.to_string()).with_context(
|
|
|
|
|
|
|
|
DiagnosticContext {
|
|
|
|
|
|
|
|
phase: Some(Phase::Read),
|
|
|
|
|
|
|
|
path: Some(archive.to_string()),
|
|
|
|
|
|
|
|
archive_entry: Some(name.to_string()),
|
|
|
|
|
|
|
|
..DiagnosticContext::default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
})?;
|
|
|
|
let Some(handle) = repository
|
|
|
|
let Some(handle) = repository
|
|
|
|
.find(archive_handle, &resource_name)
|
|
|
|
.find(archive_handle, &resource_name)
|
|
|
|
.map_err(|err| format!("{err}"))?
|
|
|
|
.map_err(|err| {
|
|
|
|
|
|
|
|
diagnostic(DiagnosticCode("S1.RESOURCE.FIND"), err.to_string()).with_context(
|
|
|
|
|
|
|
|
DiagnosticContext {
|
|
|
|
|
|
|
|
phase: Some(Phase::Resolve),
|
|
|
|
|
|
|
|
path: Some(archive.to_string()),
|
|
|
|
|
|
|
|
archive_entry: Some(name.to_string()),
|
|
|
|
|
|
|
|
..DiagnosticContext::default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
})?
|
|
|
|
else {
|
|
|
|
else {
|
|
|
|
return Err(format!(
|
|
|
|
return Err(
|
|
|
|
|
|
|
|
diagnostic(
|
|
|
|
|
|
|
|
DiagnosticCode("S1.RESOURCE.MISSING_ENTRY"),
|
|
|
|
|
|
|
|
format!(
|
|
|
|
"resource not found: {archive}/{}",
|
|
|
|
"resource not found: {archive}/{}",
|
|
|
|
String::from_utf8_lossy(name.as_bytes())
|
|
|
|
String::from_utf8_lossy(name.as_bytes())
|
|
|
|
));
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
.with_context(DiagnosticContext {
|
|
|
|
|
|
|
|
phase: Some(Phase::Resolve),
|
|
|
|
|
|
|
|
path: Some(archive.to_string()),
|
|
|
|
|
|
|
|
archive_entry: Some(name.to_string()),
|
|
|
|
|
|
|
|
..DiagnosticContext::default()
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
let bytes = repository.read(handle).map_err(|err| format!("{err}"))?;
|
|
|
|
let bytes = repository.read(handle).map_err(|err| {
|
|
|
|
|
|
|
|
diagnostic(DiagnosticCode("S1.RESOURCE.READ"), err.to_string()).with_context(
|
|
|
|
|
|
|
|
DiagnosticContext {
|
|
|
|
|
|
|
|
phase: Some(Phase::Read),
|
|
|
|
|
|
|
|
path: Some(archive.to_string()),
|
|
|
|
|
|
|
|
archive_entry: Some(name.to_string()),
|
|
|
|
|
|
|
|
..DiagnosticContext::default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
})?;
|
|
|
|
Ok(Arc::from(bytes.into_owned()))
|
|
|
|
Ok(Arc::from(bytes.into_owned()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn load_model_document_from_root(
|
|
|
|
fn load_model_document_from_root_diagnostic(
|
|
|
|
root: &Path,
|
|
|
|
root: &Path,
|
|
|
|
archive: &str,
|
|
|
|
archive: &str,
|
|
|
|
resource: &str,
|
|
|
|
resource: &str,
|
|
|
|
) -> Result<NresDocument, String> {
|
|
|
|
) -> Result<NresDocument, Diagnostic> {
|
|
|
|
let bytes = read_resource_bytes(root, archive, resource)?;
|
|
|
|
let bytes = read_resource_bytes_diagnostic(root, archive, resource)?;
|
|
|
|
decode_nres(bytes, ReadProfile::Compatible).map_err(|err| err.to_string())
|
|
|
|
decode_nres(bytes.clone(), ReadProfile::Compatible).map_err(|err| {
|
|
|
|
|
|
|
|
resource_parse_diagnostic("S1.NRES.DECODE", archive, resource, &bytes, err.to_string())
|
|
|
|
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn archive_parse_diagnostic(
|
|
|
|
fn archive_parse_diagnostic(
|
|
|
@@ -401,12 +467,35 @@ fn archive_parse_diagnostic(
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn resource_parse_diagnostic(
|
|
|
|
|
|
|
|
code: &'static str,
|
|
|
|
|
|
|
|
archive: &str,
|
|
|
|
|
|
|
|
resource: &str,
|
|
|
|
|
|
|
|
bytes: &[u8],
|
|
|
|
|
|
|
|
message: String,
|
|
|
|
|
|
|
|
) -> Diagnostic {
|
|
|
|
|
|
|
|
diagnostic(DiagnosticCode(code), message).with_context(DiagnosticContext {
|
|
|
|
|
|
|
|
phase: Some(Phase::Parse),
|
|
|
|
|
|
|
|
path: Some(archive.to_string()),
|
|
|
|
|
|
|
|
archive_entry: Some(resource.to_string()),
|
|
|
|
|
|
|
|
span: Some(SourceSpan {
|
|
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
|
|
length: u64::try_from(bytes.len().min(4)).unwrap_or(4),
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
..DiagnosticContext::default()
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use super::*;
|
|
|
|
use std::io::Write as _;
|
|
|
|
use std::io::Write as _;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const TEST_NRES_HEADER_LEN: usize = 16;
|
|
|
|
|
|
|
|
const TEST_NRES_NAME_LEN: usize = 36;
|
|
|
|
|
|
|
|
const TEST_NRES_VERSION_0100: u32 = 0x100;
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[test]
|
|
|
|
fn inspect_rsli_rejects_malformed_archive() {
|
|
|
|
fn inspect_rsli_rejects_malformed_archive() {
|
|
|
|
let dir = temp_dir("inspect");
|
|
|
|
let dir = temp_dir("inspect");
|
|
|
@@ -454,6 +543,28 @@ mod tests {
|
|
|
|
let _ = inspect_archive_file(&archive, 2);
|
|
|
|
let _ = inspect_archive_file(&archive, 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn model_archive_diagnostic_preserves_archive_entry_context() {
|
|
|
|
|
|
|
|
let dir = temp_dir("inspect-model-diagnostic");
|
|
|
|
|
|
|
|
let archive = dir.join("models.rlb");
|
|
|
|
|
|
|
|
fs::write(&archive, build_single_entry_nres(b"BROKEN.MSH", b"NRes")).expect("archive");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let diagnostic = load_model_document_from_root_diagnostic(&dir, "models.rlb", "BROKEN.MSH")
|
|
|
|
|
|
|
|
.expect_err("nested diagnostic failure");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(diagnostic.code.0, "S1.NRES.DECODE");
|
|
|
|
|
|
|
|
assert_eq!(diagnostic.context.phase, Some(Phase::Parse));
|
|
|
|
|
|
|
|
assert_eq!(diagnostic.context.path.as_deref(), Some("models.rlb"));
|
|
|
|
|
|
|
|
assert_eq!(diagnostic.context.archive_entry.as_deref(), Some("BROKEN.MSH"));
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
|
|
diagnostic.context.span,
|
|
|
|
|
|
|
|
Some(SourceSpan {
|
|
|
|
|
|
|
|
offset: 0,
|
|
|
|
|
|
|
|
length: 4
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn temp_dir(name: &str) -> PathBuf {
|
|
|
|
fn temp_dir(name: &str) -> PathBuf {
|
|
|
|
let base = PathBuf::from("/tmp")
|
|
|
|
let base = PathBuf::from("/tmp")
|
|
|
|
.join("fparkan-inspection-tests")
|
|
|
|
.join("fparkan-inspection-tests")
|
|
|
@@ -462,4 +573,34 @@ mod tests {
|
|
|
|
fs::create_dir_all(&base).expect("tmp dir");
|
|
|
|
fs::create_dir_all(&base).expect("tmp dir");
|
|
|
|
base
|
|
|
|
base
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn build_single_entry_nres(name: &[u8], payload: &[u8]) -> Vec<u8> {
|
|
|
|
|
|
|
|
let mut out = vec![0; TEST_NRES_HEADER_LEN];
|
|
|
|
|
|
|
|
let payload_offset = u32::try_from(out.len()).expect("payload offset");
|
|
|
|
|
|
|
|
out.extend_from_slice(payload);
|
|
|
|
|
|
|
|
let padding = (8 - (out.len() % 8)) % 8;
|
|
|
|
|
|
|
|
out.resize(out.len() + padding, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
push_u32(&mut out, 1);
|
|
|
|
|
|
|
|
push_u32(&mut out, 0);
|
|
|
|
|
|
|
|
push_u32(&mut out, 0);
|
|
|
|
|
|
|
|
push_u32(&mut out, u32::try_from(payload.len()).expect("payload len"));
|
|
|
|
|
|
|
|
push_u32(&mut out, 0);
|
|
|
|
|
|
|
|
let mut raw_name = [0; TEST_NRES_NAME_LEN];
|
|
|
|
|
|
|
|
raw_name[..name.len()].copy_from_slice(name);
|
|
|
|
|
|
|
|
out.extend_from_slice(&raw_name);
|
|
|
|
|
|
|
|
push_u32(&mut out, payload_offset);
|
|
|
|
|
|
|
|
push_u32(&mut out, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
out[0..4].copy_from_slice(b"NRes");
|
|
|
|
|
|
|
|
out[4..8].copy_from_slice(&TEST_NRES_VERSION_0100.to_le_bytes());
|
|
|
|
|
|
|
|
out[8..12].copy_from_slice(&1_u32.to_le_bytes());
|
|
|
|
|
|
|
|
let total_size = u32::try_from(out.len()).expect("total size");
|
|
|
|
|
|
|
|
out[12..16].copy_from_slice(&total_size.to_le_bytes());
|
|
|
|
|
|
|
|
out
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fn push_u32(out: &mut Vec<u8>, value: u32) {
|
|
|
|
|
|
|
|
out.extend_from_slice(&value.to_le_bytes());
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|