JPMS descriptors are never loaded by a class loader; parsing one and rebuilding its pool left the unmodeled Module attribute with stale indices, and ASM died in readModule on the rewritten jar (caught by the ASM probe on Loreport, which ships META-INF/versions module-infos). Byte-for-byte resource passthrough keeps the descriptor valid.
162 lines
5.5 KiB
Rust
162 lines
5.5 KiB
Rust
//! Archive to [`Program`] and back.
|
|
//!
|
|
//! The rule is simple: `*.class` goes to the parser, everything else -
|
|
//! to resources. Classes serialize first, then resources: the order inside
|
|
//! the archive does not affect behaviour, and a fixed order gives
|
|
//! reproducible writes.
|
|
|
|
use std::collections::BTreeSet;
|
|
|
|
use noxis_classfile::Program;
|
|
use noxis_classfile::format;
|
|
use noxis_classfile::model::Resource;
|
|
|
|
use crate::archive::{Entry, JarError};
|
|
|
|
/// Parses archive entries: classes - through the parser, the rest - as resources.
|
|
///
|
|
/// A name that repeats (multi-release jars carry the same class at the root
|
|
/// and under `META-INF/versions/*`) is kept once, first occurrence wins:
|
|
/// the flat model cannot hold two classes under one name, and later passes
|
|
/// (rename maps, jar writing) assume uniqueness.
|
|
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
|
let mut program = Program::default();
|
|
let mut seen: BTreeSet<String> = BTreeSet::new();
|
|
|
|
for entry in entries {
|
|
if is_class_entry(&entry.name) && !is_module_info(&entry.name) {
|
|
let class = format::parse(&entry.data)?;
|
|
if seen.insert(class.this_class.clone()) {
|
|
program.classes.push(class);
|
|
}
|
|
} else {
|
|
program.resources.push(Resource {
|
|
path: entry.name,
|
|
data: entry.data,
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(program)
|
|
}
|
|
|
|
/// Assembles archive entries from [`Program`]: classes first, then resources.
|
|
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
|
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
|
|
|
|
for class in &program.classes {
|
|
let name = format!("{}.class", class.this_class);
|
|
let bytes = format::write(class).map_err(|error| {
|
|
JarError::Classfile(noxis_classfile::ClassfileError::Write(format!(
|
|
"{}: {error}",
|
|
class.this_class
|
|
)))
|
|
})?;
|
|
entries.push(Entry::new(name, bytes));
|
|
}
|
|
for resource in &program.resources {
|
|
entries.push(Entry::new(resource.path.clone(), resource.data.clone()));
|
|
}
|
|
|
|
Ok(entries)
|
|
}
|
|
|
|
/// A class file inside the archive: the path ends in `.class`.
|
|
fn is_class_entry(name: &str) -> bool {
|
|
name.len() > ".class".len() && name.ends_with(".class")
|
|
}
|
|
|
|
/// A JPMS descriptor. No class loader ever loads it; parsing would rebuild
|
|
/// its pool and leave the unmodeled `Module` attribute with stale indices
|
|
/// (ASM then dies in `readModule`), so it travels as a raw resource with
|
|
/// its bytes intact.
|
|
fn is_module_info(name: &str) -> bool {
|
|
name.ends_with("module-info.class")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn class_paths_are_recognised() {
|
|
assert!(is_class_entry("demo/App.class"));
|
|
assert!(is_class_entry("a/b/C.class"));
|
|
assert!(!is_class_entry(".class"));
|
|
assert!(!is_class_entry("classes.txt"));
|
|
assert!(!is_class_entry("demo/App.class.bak"));
|
|
}
|
|
|
|
#[test]
|
|
fn resources_round_trip_without_classes() {
|
|
let source = vec![
|
|
Entry::new("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n".to_vec()),
|
|
Entry::new("config/app.json", br#"{"k":1}"#.to_vec()),
|
|
];
|
|
|
|
let program = load(source.clone()).expect("load");
|
|
assert!(program.classes.is_empty());
|
|
assert_eq!(program.resources.len(), 2);
|
|
|
|
assert_eq!(save(&program).expect("save"), source);
|
|
}
|
|
|
|
/// Class entries go through the parser (round-trip keeps them intact).
|
|
#[test]
|
|
fn class_entries_go_through_the_parser() {
|
|
let entries = vec![
|
|
Entry::new("demo/App.class", vec![0xCA, 0xFE]),
|
|
Entry::new("notes.txt", b"hi".to_vec()),
|
|
];
|
|
|
|
match load(entries) {
|
|
Ok(program) => {
|
|
assert_eq!(program.classes.len(), 1);
|
|
assert_eq!(program.resources.len(), 1);
|
|
}
|
|
Err(JarError::Classfile(err)) => {
|
|
assert!(!err.to_string().is_empty());
|
|
}
|
|
Err(other) => panic!("unexpected error: {other}"),
|
|
}
|
|
}
|
|
|
|
/// Multi-release twins (`demo/App.class` + `META-INF/versions/9/...`):
|
|
/// the flat model keeps the first occurrence so no two classes ever
|
|
/// share one name downstream.
|
|
#[test]
|
|
fn duplicate_class_names_keep_first() {
|
|
use noxis_classfile::ClassBuilder;
|
|
|
|
let class = ClassBuilder::new("demo/App", None).build();
|
|
let bytes = format::write(&class).expect("write");
|
|
let entries = vec![
|
|
Entry::new("demo/App.class", bytes.clone()),
|
|
Entry::new("META-INF/versions/9/demo/App.class", bytes),
|
|
];
|
|
let program = load(entries).expect("load");
|
|
assert_eq!(program.classes.len(), 1);
|
|
assert_eq!(program.classes[0].this_class, "demo/App");
|
|
}
|
|
|
|
/// JPMS descriptors are never class-loaded: they pass through as raw
|
|
/// resources (parsing would rebuild their pool and stale the `Module`
|
|
/// attribute, which stays unmodeled).
|
|
#[test]
|
|
fn module_info_passes_through_as_resource() {
|
|
let entries = vec![
|
|
Entry::new("META-INF/versions/9/module-info.class", vec![0xCA, 0xFE]),
|
|
Entry::new("notes.txt", b"hi".to_vec()),
|
|
];
|
|
|
|
let program = load(entries).expect("load");
|
|
assert!(program.classes.is_empty());
|
|
assert_eq!(program.resources.len(), 2);
|
|
assert_eq!(
|
|
program.resources[0].path,
|
|
"META-INF/versions/9/module-info.class"
|
|
);
|
|
}
|
|
}
|