Initial commit: noxis JVM bytecode obfuscator
Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis, noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check). AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Сборочные артефакты Cargo
|
||||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
# Локальный cargo-config (может содержать секреты репозитория)
|
||||
/.cargo/config.toml
|
||||
/config.toml
|
||||
|
||||
# Артефакты тестовых прогонов обфускатора
|
||||
/out
|
||||
/*.jar
|
||||
/*.class
|
||||
!tests/data/**/*.class
|
||||
|
||||
# Редакторы и ОС
|
||||
**/*.swp
|
||||
**/.idea/
|
||||
**/.vscode/
|
||||
.DS_Store
|
||||
888
Cargo.lock
generated
Normal file
888
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,888 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "noxis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"noxis-analysis",
|
||||
"noxis-classfile",
|
||||
"noxis-core",
|
||||
"noxis-jar",
|
||||
"noxis-passes",
|
||||
"noxis-structure-check",
|
||||
"noxis-verify",
|
||||
"ristretto_classfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-analysis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"noxis-classfile",
|
||||
"noxis-core",
|
||||
"noxis-structure-check",
|
||||
"regex",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-classfile"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"noxis-structure-check",
|
||||
"ristretto_classfile",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"noxis-classfile",
|
||||
"noxis-structure-check",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-jar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"noxis-classfile",
|
||||
"noxis-structure-check",
|
||||
"thiserror",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-passes"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"noxis-analysis",
|
||||
"noxis-classfile",
|
||||
"noxis-core",
|
||||
"noxis-runtime",
|
||||
"noxis-structure-check",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"noxis-classfile",
|
||||
"noxis-core",
|
||||
"noxis-structure-check",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noxis-structure-check"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "noxis-verify"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"noxis-analysis",
|
||||
"noxis-classfile",
|
||||
"noxis-core",
|
||||
"noxis-jar",
|
||||
"noxis-structure-check",
|
||||
"regex",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "ristretto_classfile"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e68fddbd2de10b9c50157aa05ca424fc6d651f0cb0944e1c56a2fe3f3d9228e1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"getrandom",
|
||||
"hashbrown",
|
||||
"indexmap",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.7+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7874cd34dd99040fc24e30923aa64a60a81ff3ddfe2b8129ccc84ba24949deed"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.4+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"typed-path",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
45
Cargo.toml
Normal file
45
Cargo.toml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[workspace.dependencies]
|
||||
noxis-analysis = { path = "crates/noxis-analysis" }
|
||||
noxis-classfile = { path = "crates/noxis-classfile" }
|
||||
noxis-core = { path = "crates/noxis-core" }
|
||||
noxis-jar = { path = "crates/noxis-jar" }
|
||||
noxis-passes = { path = "crates/noxis-passes" }
|
||||
noxis-runtime = { path = "crates/noxis-runtime" }
|
||||
noxis-structure-check = { path = "crates/noxis-structure-check" }
|
||||
noxis-verify = { path = "crates/noxis-verify" }
|
||||
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
indexmap = "2"
|
||||
regex = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
toml = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
665
LICENSE
Normal file
665
LICENSE
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
noxis — JVM/Java bytecode obfuscator
|
||||
|
||||
Copyright (C) 2026 loki5512344
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
17
NOTICE.md
Normal file
17
NOTICE.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# NOTICE — сторонние компоненты
|
||||
|
||||
Проект `noxis` распространяется по лицензии AGPL-3.0-only
|
||||
(см. [`LICENSE`](LICENSE)).
|
||||
Ниже — компоненты third-party, которые используются напрямую и чьи
|
||||
лицензионные условия обязательны к сохранению.
|
||||
|
||||
## ristretto_classfile
|
||||
|
||||
- **Источник:** <https://github.com/theseus-rs/ristretto>
|
||||
- **Версия:** 0.34.0 (зависимость crates.io, код не вендорится)
|
||||
- **Лицензия:** Apache-2.0 OR MIT (на выбор)
|
||||
- **Использование:** чтение и запись `.class` (`format::parse` /
|
||||
`format::write`), приёмочная верификация `verify_class(Strict)`.
|
||||
|
||||
Компонент подключается как обычная cargo-зависимость; исходные тексты и
|
||||
лицензии хранятся в реестре (`~/.cargo/registry`) и в `Cargo.lock`.
|
||||
110
README.md
Normal file
110
README.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# noxis
|
||||
|
||||
Обфускатор JVM/Java-байткода на Rust.
|
||||
|
||||
Читает JAR, переписывает классовые файлы и ресурсы, собирает обратно —
|
||||
детерминированно, с маппингами для stack trace и с проверкой, что выход
|
||||
действительно загружается настоящей JVM.
|
||||
|
||||
## Статус
|
||||
|
||||
В работе. Ядро построено, часть техник реализована, часть — по плану.
|
||||
|
||||
| блок | состояние |
|
||||
|---|---|
|
||||
| IR классовых файлов, метки вместо смещений | ✅ |
|
||||
| конфиг, детерминированный PRNG, маппинги, отчёт | ✅ |
|
||||
| анализ: иерархия, entrypoint'ы, keep-правила | ✅ |
|
||||
| переименование всех символов | ✅ |
|
||||
| схемы шифрования строк (salt, nonce, MAC) | ✅ |
|
||||
| JAR: чтение/запись, ресурсы, срез подписей | ✅ |
|
||||
| гейт структурных правил при компиляции | ✅ |
|
||||
| пассы: rename, resources, debug | ✅ |
|
||||
| spike backend'а `ristretto_classfile` | ✅ |
|
||||
| `format::parse`/`write` + кадры `StackMapTable` | ✅ |
|
||||
| пассы: strings, constants, noise (+ декодер v49) | ✅ |
|
||||
| пасс flow | 🚧 |
|
||||
| верификация и метрики | 🚧 |
|
||||
| CLI | 🚧 |
|
||||
|
||||
Подробности — [`docs/architecture.md`](docs/architecture.md),
|
||||
план — [`todo.md`](todo.md).
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
noxis/
|
||||
├── Cargo.toml workspace, lints, release-профиль
|
||||
├── scripts/check.sh structure → fmt → clippy → test
|
||||
├── docs/ ← архитектура, техники, стек, уроки
|
||||
├── info/ исходный материал разбора (вне правил)
|
||||
└── crates/
|
||||
├── noxis-classfile/ IR + парсер/запись + StackMapTable
|
||||
├── noxis-core/ конфиг, seed, маппинги, контракт Pass
|
||||
├── noxis-analysis/ иерархия, entrypoint'ы, keep
|
||||
├── noxis-passes/ сами трансформации + конвейер
|
||||
├── noxis-runtime/ схемы шифрования, ABI декодера
|
||||
├── noxis-jar/ ZIP, ресурсы, срез подписей
|
||||
├── noxis-verify/ red-паттерны, java -Xverify (план)
|
||||
├── noxis/ CLI (план)
|
||||
└── noxis-structure-check/ автогейт структуры
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```sh
|
||||
./scripts/check.sh # fmt + clippy -D warnings + тесты
|
||||
```
|
||||
|
||||
Одно `cargo build` уже гоняет проверку структуры проекта: если файл
|
||||
длиннее 250 строк или в директории больше 4 файлов, сборка падает.
|
||||
Правила — в [`docs/README.md`](docs/README.md).
|
||||
|
||||
## Документация
|
||||
|
||||
| документ | что внутри |
|
||||
|---|---|
|
||||
| [`docs/README.md`](docs/README.md) | индекс и структурные правила проекта |
|
||||
| [`docs/architecture.md`](docs/architecture.md) | слои, крейты, IR, конвейер |
|
||||
| [`docs/techniques.md`](docs/techniques.md) | как работает каждая техника: плюсы и минусы |
|
||||
| [`docs/stack.md`](docs/stack.md) | почему свой IR, backend, `StackMapTable`, PRNG |
|
||||
| [`docs/learnings/lessons.md`](docs/learnings/lessons.md) | уроки разбора: чего делать нельзя |
|
||||
| [`docs/learnings/reference.md`](docs/learnings/reference.md) | разбор референсного обфускатора |
|
||||
| [`docs/learnings/priorities.md`](docs/learnings/priorities.md) | что улучшаем и в каком порядке |
|
||||
|
||||
Исходный материал разбора — [`info/README.md`](info/README.md).
|
||||
|
||||
## Принципы
|
||||
|
||||
- **KISS / DRY / SOLID.** Файл — одна ответственность. Не больше 250 строк,
|
||||
не больше 4 файлов в директории.
|
||||
- **Детерминированность.** Один seed → байт-в-байт одинаковый артефакт.
|
||||
- **Ничего не ломаем.** Техника, которая может дать `VerifyError`,
|
||||
включается только после верификатора.
|
||||
- **Измеримо.** Защита без метрик — это мнение, а не свойство.
|
||||
|
||||
## Лицензия
|
||||
|
||||
AGPL-3.0-only — см. [`LICENSE`](LICENSE).
|
||||
|
||||
Проект остаётся полностью открытым: его можно изучать, использовать
|
||||
(в том числе коммерчески) и изменять. Условие AGPL — любые производные
|
||||
работы, которые распространяются или доступны пользователям по сети,
|
||||
обязаны публиковаться под той же лицензией. Это защищает `noxis` от
|
||||
закрытых форков, которые перепродают его как готовый продукт.
|
||||
|
||||
Сторонние зависимости и их лицензии — в [`NOTICE.md`](NOTICE.md).
|
||||
|
||||
## Ответственное использование
|
||||
|
||||
`noxis` — инструмент двойного назначения. Он одинаково полезен для
|
||||
защиты собственного байткода и способен усложнять анализ вредоносного
|
||||
ПО. Лицензия это не регламентирует, но это норма сообщества проекта:
|
||||
|
||||
- используйте обфускацию, чтобы защищать **свой** код и код тех, кто
|
||||
вас об этом попросил;
|
||||
- не применяйте инструмент для упаковки вредоносов, обхода средств
|
||||
защиты и антивирусов или иной деятельности, запрещённой законом.
|
||||
|
||||
Нарушение этих норм не влечёт юридических последствий по лицензии, но
|
||||
приведёт к отказу в поддержке и закрытию issue/PR в репозитории.
|
||||
20
crates/noxis-analysis/Cargo.toml
Normal file
20
crates/noxis-analysis/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "noxis-analysis"
|
||||
description = "Иерархия классов, entrypoint'ы, reflection-сайты, keep-правила"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
regex = { workspace = true }
|
||||
noxis-classfile = { workspace = true }
|
||||
noxis-core = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis-analysis/build.rs
Normal file
3
crates/noxis-analysis/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
244
crates/noxis-analysis/src/entrypoints.rs
Normal file
244
crates/noxis-analysis/src/entrypoints.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
//! Entrypoint'ы и точки, где имя класса/метода видно снаружи.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use noxis_classfile::Program;
|
||||
use noxis_classfile::insn::{Const, Insn};
|
||||
|
||||
/// Всё, что нельзя переименовывать «как есть» и что нужно держать в keep-сете.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Entrypoints {
|
||||
/// Классы с `public static void main(String[])`.
|
||||
pub main_classes: BTreeSet<String>,
|
||||
/// `native`-методы: имя связано с JNI-схемой (`Java_pkg_Class_method`).
|
||||
pub native_methods: BTreeSet<(String, String, String)>,
|
||||
/// Интерфейсы из `META-INF/services/*`.
|
||||
pub service_interfaces: BTreeSet<String>,
|
||||
/// Имена, встречающиеся в рефлексивных вызовах (`Class.forName` и т.п.).
|
||||
pub reflective_names: BTreeSet<String>,
|
||||
/// Имена членов (`getMethod`/`getField`-литералы): их нельзя переименовывать.
|
||||
pub reflective_member_names: BTreeSet<String>,
|
||||
/// Поля с флагом `transient`+`serialVersionUID`-контекст: сериализуемые классы
|
||||
/// обязаны сохранить имя класса и полей (иначе ломается Java-сериализация).
|
||||
pub serializable_classes: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Entrypoints {
|
||||
/// Собрать entrypoint'ы по программе (без keep-правил — их добавляет [`Self::merge`]).
|
||||
pub fn collect(program: &Program) -> Self {
|
||||
let mut out = Self::default();
|
||||
|
||||
for class in &program.classes {
|
||||
if class.this_class == "java/lang/Object" {
|
||||
continue;
|
||||
}
|
||||
if implements_serializable(program, &class.this_class) {
|
||||
out.serializable_classes.insert(class.this_class.clone());
|
||||
}
|
||||
for method in &class.methods {
|
||||
const ACC_STATIC: u16 = 0x0008;
|
||||
const ACC_NATIVE: u16 = 0x0100;
|
||||
if method.access_flags & ACC_NATIVE != 0 {
|
||||
out.native_methods.insert((
|
||||
class.this_class.clone(),
|
||||
method.name.clone(),
|
||||
method.descriptor.clone(),
|
||||
));
|
||||
}
|
||||
if method.name == "main"
|
||||
&& method.descriptor == "([Ljava/lang/String;)V"
|
||||
&& method.access_flags & ACC_STATIC != 0
|
||||
{
|
||||
out.main_classes.insert(class.this_class.clone());
|
||||
}
|
||||
if let Some(code) = &method.code {
|
||||
collect_reflective_names(&mut out, code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.service_interfaces = collect_service_interfaces(program);
|
||||
out
|
||||
}
|
||||
|
||||
/// Объединить с другим набором (например, из keep-правил конфига).
|
||||
pub fn merge(&mut self, other: &Entrypoints) {
|
||||
self.main_classes.extend(other.main_classes.iter().cloned());
|
||||
self.native_methods
|
||||
.extend(other.native_methods.iter().cloned());
|
||||
self.service_interfaces
|
||||
.extend(other.service_interfaces.iter().cloned());
|
||||
self.reflective_names
|
||||
.extend(other.reflective_names.iter().cloned());
|
||||
self.reflective_member_names
|
||||
.extend(other.reflective_member_names.iter().cloned());
|
||||
self.serializable_classes
|
||||
.extend(other.serializable_classes.iter().cloned());
|
||||
}
|
||||
|
||||
/// Класс, который нельзя переименовывать.
|
||||
pub fn keeps_class(&self, internal_name: &str) -> bool {
|
||||
self.main_classes.contains(internal_name)
|
||||
|| self.serializable_classes.contains(internal_name)
|
||||
|| self.reflective_names.contains(internal_name)
|
||||
}
|
||||
|
||||
/// Член (метод/поле), который нельзя переименовывать.
|
||||
pub fn keeps_member(&self, name: &str) -> bool {
|
||||
self.reflective_member_names.contains(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// `implements java.io.Serializable` (по цепочке предков, в пределах программы).
|
||||
fn implements_serializable(program: &Program, start: &str) -> bool {
|
||||
let mut stack = vec![start.to_string()];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
while let Some(name) = stack.pop() {
|
||||
if !seen.insert(name.clone()) {
|
||||
continue;
|
||||
}
|
||||
if name == "java/io/Serializable" {
|
||||
return true;
|
||||
}
|
||||
let Some(class) = program.class(&name) else {
|
||||
// Внешний предок: считаем, что не знаем — консервативно не keep'им.
|
||||
continue;
|
||||
};
|
||||
if let Some(sup) = &class.super_class {
|
||||
stack.push(sup.clone());
|
||||
}
|
||||
stack.extend(class.interfaces.iter().cloned());
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `META-INF/services/<iface>` → внутреннее имя интерфейса.
|
||||
fn collect_service_interfaces(program: &Program) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
for resource in &program.resources {
|
||||
let Some(name) = resource.path.strip_prefix("META-INF/services/") else {
|
||||
continue;
|
||||
};
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
out.insert(name.replace('.', "/"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Линейный эвристический поиск имён в рефлексивных API.
|
||||
///
|
||||
/// Полноценный dataflow не нужен: если рядом с вызовом `Class.forName(...)` /
|
||||
/// `getMethod(...)` лежит строковый литерал — это имя видно статически,
|
||||
/// значит, его нельзя переименовывать.
|
||||
fn collect_reflective_names(out: &mut Entrypoints, code: &noxis_classfile::Code) {
|
||||
/// API, чьи аргументы — **имена классов**.
|
||||
const CLASS_API: &[(&str, &str)] = &[
|
||||
("java/lang/Class", "forName"),
|
||||
("java/lang/ClassLoader", "loadClass"),
|
||||
];
|
||||
/// API, чьи аргументы — **имена членов** (поле/метод).
|
||||
const MEMBER_API: &[(&str, &str)] = &[
|
||||
("java/lang/Class", "getMethod"),
|
||||
("java/lang/Class", "getDeclaredMethod"),
|
||||
("java/lang/Class", "getField"),
|
||||
("java/lang/Class", "getDeclaredField"),
|
||||
];
|
||||
|
||||
let mut last_string: Option<&str> = None;
|
||||
for insn in &code.insns {
|
||||
if let Insn::Const(Const::String(s)) = insn {
|
||||
last_string = Some(s.as_str());
|
||||
continue;
|
||||
}
|
||||
if let Insn::Invoke { owner, name, .. } = insn {
|
||||
let is_class_api = CLASS_API.iter().any(|(o, n)| o == owner && n == name);
|
||||
let is_member_api = MEMBER_API.iter().any(|(o, n)| o == owner && n == name);
|
||||
if let (true, Some(s)) = (is_class_api, last_string) {
|
||||
out.reflective_names.insert(s.replace('.', "/"));
|
||||
}
|
||||
if let (true, Some(s)) = (is_member_api, last_string) {
|
||||
out.reflective_member_names.insert(s.to_string());
|
||||
}
|
||||
last_string = None;
|
||||
continue;
|
||||
}
|
||||
// Инструкции, не относящиеся к строке/вызову, обрывают «соседность».
|
||||
if !matches!(insn, Insn::Mark(_) | Insn::Nop) {
|
||||
last_string = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
use noxis_classfile::insn::InvokeKind;
|
||||
use noxis_classfile::{ClassBuilder, Insn, empty_code};
|
||||
|
||||
#[test]
|
||||
fn finds_main_and_native() {
|
||||
let main = ClassBuilder::new("app/Main", None)
|
||||
.method_with_code(0x0009, "main", "([Ljava/lang/String;)V", empty_code(1))
|
||||
.build();
|
||||
let mut native_class = ClassBuilder::new("app/Jni", None).build();
|
||||
native_class.methods.push(noxis_classfile::Method {
|
||||
access_flags: 0x0100, // ACC_NATIVE
|
||||
name: "ping".into(),
|
||||
descriptor: "()I".into(),
|
||||
code: None,
|
||||
attributes: Vec::new(),
|
||||
});
|
||||
let program = Program {
|
||||
classes: vec![main, native_class],
|
||||
resources: Vec::new(),
|
||||
};
|
||||
let ep = Entrypoints::collect(&program);
|
||||
assert!(ep.main_classes.contains("app/Main"));
|
||||
assert!(ep.native_methods.contains(&(
|
||||
"app/Jni".to_string(),
|
||||
"ping".to_string(),
|
||||
"()I".to_string()
|
||||
)));
|
||||
assert!(ep.keeps_class("app/Main"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collects_service_interface() {
|
||||
let program = Program {
|
||||
classes: Vec::new(),
|
||||
resources: vec![noxis_classfile::Resource {
|
||||
path: "META-INF/services/com.example.Plugin".into(),
|
||||
data: b"app.Impl\n".to_vec(),
|
||||
}],
|
||||
};
|
||||
let ep = Entrypoints::collect(&program);
|
||||
assert!(ep.service_interfaces.contains("com/example/Plugin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_reflective_literal() {
|
||||
let mut code = empty_code(1);
|
||||
code.insns = vec![
|
||||
Insn::Const(Const::String("app.Secret".into())),
|
||||
Insn::Invoke {
|
||||
kind: InvokeKind::Static,
|
||||
owner: "java/lang/Class".into(),
|
||||
name: "forName".into(),
|
||||
descriptor: "(Ljava/lang/String;)Ljava/lang/Class;".into(),
|
||||
itf: false,
|
||||
},
|
||||
];
|
||||
let class = ClassBuilder::new("app/Caller", None)
|
||||
.method_with_code(0x0009, "go", "()V", code)
|
||||
.build();
|
||||
let program = Program {
|
||||
classes: vec![class],
|
||||
resources: Vec::new(),
|
||||
};
|
||||
let ep = Entrypoints::collect(&program);
|
||||
assert!(ep.reflective_names.contains("app/Secret"));
|
||||
}
|
||||
}
|
||||
250
crates/noxis-analysis/src/hierarchy.rs
Normal file
250
crates/noxis-analysis/src/hierarchy.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
//! Иерархия классов: наследование, переопределения, безопасность переименования.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use noxis_classfile::Program;
|
||||
|
||||
/// Узел иерархии: суперкласс и интерфейсы (по внутренним именам).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HierarchyNode {
|
||||
pub super_class: Option<String>,
|
||||
pub interfaces: Vec<String>,
|
||||
}
|
||||
|
||||
/// Построенная иерархия классов.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ClassHierarchy {
|
||||
nodes: HashMap<String, HierarchyNode>,
|
||||
}
|
||||
|
||||
impl ClassHierarchy {
|
||||
/// Построить иерархию из программы (только по имеющимся классам).
|
||||
pub fn build(program: &Program) -> Self {
|
||||
let mut nodes = HashMap::with_capacity(program.classes.len());
|
||||
for class in &program.classes {
|
||||
nodes.insert(
|
||||
class.this_class.clone(),
|
||||
HierarchyNode {
|
||||
super_class: class.super_class.clone(),
|
||||
interfaces: class.interfaces.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Self { nodes }
|
||||
}
|
||||
|
||||
/// Прямой суперкласс (для LUB при выводе кадров).
|
||||
pub fn superclass(&self, internal_name: &str) -> Option<String> {
|
||||
self.nodes
|
||||
.get(internal_name)
|
||||
.and_then(|n| n.super_class.clone())
|
||||
}
|
||||
|
||||
/// Есть ли класс в иерархии (в том числе из внешнего classpath — нет).
|
||||
pub fn contains(&self, internal_name: &str) -> bool {
|
||||
self.nodes.contains_key(internal_name)
|
||||
}
|
||||
|
||||
/// Прямые предки: суперкласс + интерфейсы.
|
||||
pub fn parents(&self, internal_name: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(node) = self.nodes.get(internal_name) {
|
||||
out.extend(node.super_class.iter().cloned());
|
||||
out.extend(node.interfaces.iter().cloned());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Является ли `child` подклассом `parent` (в пределах программы).
|
||||
///
|
||||
/// Неполная иерархия (внешние зависимости не грузим) — это осознанно:
|
||||
/// обфускатор работает по артефакту, а не по всему classpath.
|
||||
pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
|
||||
if child == parent {
|
||||
return true;
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
let mut stack = vec![child.to_string()];
|
||||
while let Some(current) = stack.pop() {
|
||||
if !seen.insert(current.clone()) {
|
||||
continue;
|
||||
}
|
||||
for p in self.parents(¤t) {
|
||||
if p == parent {
|
||||
return true;
|
||||
}
|
||||
stack.push(p);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Все классы программы, которые наследуют `parent` (включая его самого).
|
||||
pub fn descendants(&self, parent: &str) -> Vec<String> {
|
||||
self.nodes
|
||||
.keys()
|
||||
.filter(|name| self.is_subclass_of(name, parent))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Классы, объявляющие метод с данным именем/дескриптором, вверх по цепочке.
|
||||
pub fn declaring_chain(
|
||||
&self,
|
||||
program: &Program,
|
||||
start: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
let mut stack = vec![start.to_string()];
|
||||
while let Some(current) = stack.pop() {
|
||||
if !seen.insert(current.clone()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(class) = program.class(¤t)
|
||||
&& class.method(name, descriptor).is_some()
|
||||
{
|
||||
out.push(current.clone());
|
||||
}
|
||||
stack.extend(self.parents(¤t));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl noxis_classfile::Hierarchy for ClassHierarchy {
|
||||
fn superclass(&self, internal_name: &str) -> Option<String> {
|
||||
self.superclass(internal_name)
|
||||
}
|
||||
}
|
||||
/// Почему метод нельзя/можно переименовывать (аналог `InheritanceAnalyzer`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RenameSafety {
|
||||
/// Переименовывать можно: это точка, где имя не видно снаружи.
|
||||
Safe,
|
||||
/// Конструктор/статический блок: имя зафиксировано форматом JVM.
|
||||
FixedName,
|
||||
/// Виртуальный метод, который может переопределяться вне артефакта.
|
||||
VirtualExternal,
|
||||
/// Рефлексия/сериализация может обратиться по имени.
|
||||
ReferencedByName,
|
||||
/// Метод не найден в классе.
|
||||
Missing,
|
||||
}
|
||||
|
||||
/// Оценка безопасности переименования метода.
|
||||
///
|
||||
/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`):
|
||||
/// - `<init>`/`<clinit>` — никогда;
|
||||
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса — можно;
|
||||
/// - `public`/`protected`/`package-private`, если класс не `final` и есть
|
||||
/// наследники (внутри артефакта или нет) — нельзя, пока нет доказательства.
|
||||
pub fn method_rename_safety(
|
||||
program: &Program,
|
||||
hierarchy: &ClassHierarchy,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
externally_referenced: bool,
|
||||
) -> RenameSafety {
|
||||
if name == "<init>" || name == "<clinit>" {
|
||||
return RenameSafety::FixedName;
|
||||
}
|
||||
let Some(class) = program.class(owner) else {
|
||||
return RenameSafety::Missing;
|
||||
};
|
||||
if externally_referenced {
|
||||
return RenameSafety::ReferencedByName;
|
||||
}
|
||||
let Some(method) = class.method(name, descriptor) else {
|
||||
return RenameSafety::Missing;
|
||||
};
|
||||
|
||||
const ACC_PRIVATE: u16 = 0x0002;
|
||||
const ACC_STATIC: u16 = 0x0008;
|
||||
const ACC_FINAL: u16 = 0x0010;
|
||||
const ACC_PUBLIC: u16 = 0x0001;
|
||||
const ACC_PROTECTED: u16 = 0x0004;
|
||||
|
||||
let flags = method.access_flags;
|
||||
if flags & ACC_PRIVATE != 0 || flags & ACC_STATIC != 0 || flags & ACC_FINAL != 0 {
|
||||
return RenameSafety::Safe;
|
||||
}
|
||||
|
||||
// Не-private без `final`: ищем переопределения в иерархии.
|
||||
let chain = hierarchy.declaring_chain(program, owner, name, descriptor);
|
||||
let has_subclasses = chain
|
||||
.iter()
|
||||
.any(|c| c != owner && hierarchy.is_subclass_of(c, owner));
|
||||
if has_subclasses {
|
||||
return RenameSafety::VirtualExternal;
|
||||
}
|
||||
if flags & (ACC_PUBLIC | ACC_PROTECTED) != 0 {
|
||||
// Публичный метод финального/листьевого класса: внутри артефакта
|
||||
// переопределений нет, но снаружи могут зваться по имени.
|
||||
return RenameSafety::VirtualExternal;
|
||||
}
|
||||
RenameSafety::Safe
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
use noxis_classfile::ClassBuilder;
|
||||
|
||||
fn sample_program() -> Program {
|
||||
let base = ClassBuilder::new("a/Base", None)
|
||||
.method_with_code(0x0001, "open", "()V", noxis_classfile::empty_code(0))
|
||||
.method_with_code(0x0002, "hidden", "()V", noxis_classfile::empty_code(0))
|
||||
.method_with_code(0x0001, "<init>", "()V", noxis_classfile::empty_code(0))
|
||||
.build();
|
||||
let derived = ClassBuilder::new("a/Der", Some("a/Base"))
|
||||
.method_with_code(0x0001, "open", "()V", noxis_classfile::empty_code(0))
|
||||
.build();
|
||||
Program {
|
||||
classes: vec![base, derived],
|
||||
resources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hierarchy_links_superclass() {
|
||||
let program = sample_program();
|
||||
let h = ClassHierarchy::build(&program);
|
||||
assert!(h.is_subclass_of("a/Der", "a/Base"));
|
||||
assert!(!h.is_subclass_of("a/Base", "a/Der"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_method_is_safe() {
|
||||
let program = sample_program();
|
||||
let h = ClassHierarchy::build(&program);
|
||||
assert_eq!(
|
||||
method_rename_safety(&program, &h, "a/Base", "hidden", "()V", false),
|
||||
RenameSafety::Safe
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constructor_is_never_renamed() {
|
||||
let program = sample_program();
|
||||
let h = ClassHierarchy::build(&program);
|
||||
assert_eq!(
|
||||
method_rename_safety(&program, &h, "a/Base", "<init>", "()V", false),
|
||||
RenameSafety::FixedName
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtual_method_with_subclass_is_unsafe() {
|
||||
let program = sample_program();
|
||||
let h = ClassHierarchy::build(&program);
|
||||
assert_eq!(
|
||||
method_rename_safety(&program, &h, "a/Base", "open", "()V", false),
|
||||
RenameSafety::VirtualExternal
|
||||
);
|
||||
}
|
||||
}
|
||||
237
crates/noxis-analysis/src/keep.rs
Normal file
237
crates/noxis-analysis/src/keep.rs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
//! Keep-правила: что нельзя переименовывать (glob-паттерны + entrypoint'ы).
|
||||
|
||||
use regex::Regex;
|
||||
use noxis_classfile::{Attribute, Const, Program};
|
||||
|
||||
use crate::entrypoints::Entrypoints;
|
||||
use noxis_core::Config;
|
||||
|
||||
/// Скомпилированный glob: `*` — любые символы кроме `/`, `**` — всё, `?` — один.
|
||||
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
|
||||
let mut out = String::from("^");
|
||||
let chars: Vec<char> = pattern.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
match chars[i] {
|
||||
'*' => {
|
||||
if i + 1 < chars.len() && chars[i + 1] == '*' {
|
||||
out.push_str(".*");
|
||||
i += 2;
|
||||
} else {
|
||||
out.push_str("[^/]*");
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
'?' => {
|
||||
out.push_str("[^/]");
|
||||
i += 1;
|
||||
}
|
||||
c => {
|
||||
out.push_str(®ex::escape(&c.to_string()));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push('$');
|
||||
Regex::new(&out)
|
||||
}
|
||||
|
||||
/// Правила «не трогать»: явные паттерны конфига + entrypoint'ы.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct KeepRules {
|
||||
class_patterns: Vec<Regex>,
|
||||
resource_patterns: Vec<Regex>,
|
||||
/// Точные имена, которые нельзя трогать (entrypoint'ы, рефлексия, ...).
|
||||
fixed: std::collections::BTreeSet<String>,
|
||||
/// Имена членов, защищённые рефлексией.
|
||||
fixed_members: std::collections::BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl KeepRules {
|
||||
/// Собрать правила из конфига.
|
||||
pub fn from_config(config: &Config) -> Result<Self, RuleError> {
|
||||
let mut class_patterns = Vec::with_capacity(config.keep.len());
|
||||
for pattern in &config.keep {
|
||||
class_patterns.push(compile(pattern).map_err(|e| RuleError::Pattern {
|
||||
pattern: pattern.clone(),
|
||||
msg: e.to_string(),
|
||||
})?);
|
||||
}
|
||||
let mut resource_patterns = Vec::with_capacity(config.keep_resources.len());
|
||||
for pattern in &config.keep_resources {
|
||||
resource_patterns.push(compile(pattern).map_err(|e| RuleError::Pattern {
|
||||
pattern: pattern.clone(),
|
||||
msg: e.to_string(),
|
||||
})?);
|
||||
}
|
||||
Ok(Self {
|
||||
class_patterns,
|
||||
resource_patterns,
|
||||
fixed: std::collections::BTreeSet::new(),
|
||||
fixed_members: std::collections::BTreeSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Запретить переименование члена с таким именем (в любом классе).
|
||||
pub fn keep_member(&mut self, name: impl Into<String>) {
|
||||
self.fixed_members.insert(name.into());
|
||||
}
|
||||
|
||||
/// Добавить точное имя класса (из анализа программы).
|
||||
pub fn keep_exact(&mut self, internal_name: impl Into<String>) {
|
||||
self.fixed.insert(internal_name.into());
|
||||
}
|
||||
|
||||
/// Объединить с найденными entrypoint'ами.
|
||||
pub fn absorb(&mut self, entrypoints: &Entrypoints) {
|
||||
for name in &entrypoints.main_classes {
|
||||
self.keep_exact(name.clone());
|
||||
}
|
||||
for name in &entrypoints.reflective_names {
|
||||
self.keep_exact(name.clone());
|
||||
}
|
||||
for name in &entrypoints.serializable_classes {
|
||||
self.keep_exact(name.clone());
|
||||
}
|
||||
for (owner, _, _) in &entrypoints.native_methods {
|
||||
self.keep_exact(owner.clone());
|
||||
}
|
||||
for name in &entrypoints.service_interfaces {
|
||||
self.keep_exact(name.clone());
|
||||
}
|
||||
for name in &entrypoints.reflective_member_names {
|
||||
self.keep_member(name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Методы, на которые ссылаются `BootstrapMethods` (`invokedynamic`).
|
||||
///
|
||||
/// Переименование такого метода без правки bootstrap-записи ломает класс,
|
||||
/// а правка — задача P2 (см. `todo.md`): пока просто не трогаем имена.
|
||||
pub fn absorb_bootstrap(&mut self, program: &Program) {
|
||||
for class in &program.classes {
|
||||
for method in &class.methods {
|
||||
for attr in &method.attributes {
|
||||
if let Attribute::BootstrapMethods(methods) = attr {
|
||||
for bsm in methods {
|
||||
self.keep_member(bsm.name.clone());
|
||||
for arg in &bsm.args {
|
||||
if let Const::MethodHandle { name, .. } = arg {
|
||||
self.keep_member(name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Нужно ли оставить класс с данным внутренним именем.
|
||||
pub fn keeps_class(&self, internal_name: &str) -> bool {
|
||||
self.fixed.contains(internal_name)
|
||||
|| self
|
||||
.class_patterns
|
||||
.iter()
|
||||
.any(|re| re.is_match(internal_name))
|
||||
}
|
||||
|
||||
/// Нужно ли оставить член с таким именем (не переименовывать).
|
||||
pub fn keeps_member(&self, name: &str) -> bool {
|
||||
self.fixed_members.contains(name)
|
||||
}
|
||||
|
||||
/// Нужно ли оставить ресурс с данным путём.
|
||||
pub fn keeps_resource(&self, path: &str) -> bool {
|
||||
self.resource_patterns.iter().any(|re| re.is_match(path))
|
||||
}
|
||||
|
||||
/// Число правил (для отчёта).
|
||||
pub fn len(&self) -> usize {
|
||||
self.class_patterns.len()
|
||||
+ self.resource_patterns.len()
|
||||
+ self.fixed.len()
|
||||
+ self.fixed_members.len()
|
||||
}
|
||||
|
||||
/// Есть ли вообще правила.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Ошибка компиляции паттерна.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RuleError {
|
||||
#[error("некорректный паттерн `{pattern}`: {msg}")]
|
||||
Pattern { pattern: String, msg: String },
|
||||
}
|
||||
|
||||
/// Полный анализ перед обфускацией.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Analysis {
|
||||
pub entrypoints: Entrypoints,
|
||||
pub keep: KeepRules,
|
||||
/// Классы, которые не переименовываются.
|
||||
pub locked_classes: std::collections::BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Analysis {
|
||||
/// Прогнать весь анализ по программе.
|
||||
pub fn run(program: &Program, config: &Config) -> Result<Self, RuleError> {
|
||||
let entrypoints = Entrypoints::collect(program);
|
||||
let mut keep = KeepRules::from_config(config)?;
|
||||
keep.absorb(&entrypoints);
|
||||
keep.absorb_bootstrap(program);
|
||||
|
||||
let locked_classes = program
|
||||
.classes
|
||||
.iter()
|
||||
.map(|c| c.this_class.clone())
|
||||
.filter(|name| keep.keeps_class(name))
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
entrypoints,
|
||||
keep,
|
||||
locked_classes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Класс запрещено переименовывать.
|
||||
pub fn is_locked(&self, internal_name: &str) -> bool {
|
||||
self.locked_classes.contains(internal_name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn glob_semantics() {
|
||||
let rules = KeepRules {
|
||||
class_patterns: vec![
|
||||
compile("com/example/*").unwrap(),
|
||||
compile("**/internal/**").unwrap(),
|
||||
],
|
||||
resource_patterns: vec![compile("META-INF/services/*").unwrap()],
|
||||
fixed: std::collections::BTreeSet::new(),
|
||||
fixed_members: std::collections::BTreeSet::new(),
|
||||
};
|
||||
assert!(rules.keeps_class("com/example/Main"));
|
||||
assert!(!rules.keeps_class("com/example/sub/Main"));
|
||||
assert!(rules.keeps_class("x/internal/y/Z"));
|
||||
assert!(rules.keeps_resource("META-INF/services/a.b.C"));
|
||||
assert!(!rules.keeps_resource("assets/mc/mod.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_names_win() {
|
||||
let mut rules = KeepRules::default();
|
||||
rules.keep_exact("app/Main");
|
||||
assert!(rules.keeps_class("app/Main"));
|
||||
assert!(!rules.keeps_class("app/Other"));
|
||||
}
|
||||
}
|
||||
17
crates/noxis-analysis/src/lib.rs
Normal file
17
crates/noxis-analysis/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//! # noxis-analysis
|
||||
//!
|
||||
//! Анализ программы перед обфускацией: иерархия наследования, entrypoint'ы,
|
||||
//! рефлексивные обращения по имени, keep-правила.
|
||||
//!
|
||||
//! Из этих данных пасс переименования узнаёт, **что** трогать нельзя.
|
||||
//! Урок из `info/evo-weaknesses.md`: у EvoVis рефлексия по имени была
|
||||
//! открытым текстом, а entrypoint'ы — в `fabric.mod.json`; здесь они
|
||||
//! обязаны попадать в keep-набор автоматически.
|
||||
|
||||
pub mod entrypoints;
|
||||
pub mod hierarchy;
|
||||
pub mod keep;
|
||||
|
||||
pub use entrypoints::Entrypoints;
|
||||
pub use hierarchy::{ClassHierarchy, RenameSafety, method_rename_safety};
|
||||
pub use keep::{Analysis, KeepRules, RuleError};
|
||||
18
crates/noxis-classfile/Cargo.toml
Normal file
18
crates/noxis-classfile/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "noxis-classfile"
|
||||
description = "Модель классовых файлов JVM и собственный IR обфускатора"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
indexmap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ristretto_classfile = "0.34"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis-classfile/build.rs
Normal file
3
crates/noxis-classfile/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
17
crates/noxis-classfile/src/error.rs
Normal file
17
crates/noxis-classfile/src/error.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//! Ошибки разбора/записи классовых файлов.
|
||||
|
||||
/// Ошибка на уровне одного классового файла.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ClassfileError {
|
||||
#[error("не удалось разобрать классовый файл: {0}")]
|
||||
Parse(String),
|
||||
|
||||
#[error("не удалось записать классовый файл: {0}")]
|
||||
Write(String),
|
||||
|
||||
#[error("некорректный байткод {class}: {msg}")]
|
||||
InvalidCode { class: String, msg: String },
|
||||
|
||||
#[error("не реализовано: {0}")]
|
||||
Unimplemented(&'static str),
|
||||
}
|
||||
121
crates/noxis-classfile/src/format/frames/emit.rs
Normal file
121
crates/noxis-classfile/src/format/frames/emit.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
//! Эмиссия компактных кадров в точках, где их ждёт верификатор.
|
||||
//!
|
||||
//! Вид кадра выбирается сравнением с предыдущим кадром (`Same`/`Append`/
|
||||
//! `Chop`/`SameLocals`, иначе `Full`) — как у `javac`. Хвостовые `Top`
|
||||
//! при сравнении и записи отрезаются: пустой слот — не тип.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::merge::Merger;
|
||||
use super::xfer::State;
|
||||
use super::{StackMapFrame, VerificationType};
|
||||
use crate::error::ClassfileError;
|
||||
|
||||
/// Кадры во всех точках `targets` (отсортированы — дельты по порядку).
|
||||
///
|
||||
/// Дельты — в номерах инструкций (как backend после чтения); writer сам
|
||||
/// пересчитает их в байты.
|
||||
pub(super) fn emit_frames(
|
||||
states: &[Option<State>],
|
||||
targets: &BTreeSet<u32>,
|
||||
initial: &[VerificationType],
|
||||
merger: &Merger<'_>,
|
||||
) -> Result<Vec<StackMapFrame>, ClassfileError> {
|
||||
let mut out = Vec::new();
|
||||
let mut previous_locals = initial.to_vec();
|
||||
let mut previous_index: Option<u32> = None;
|
||||
for target in targets {
|
||||
let at = *target as usize;
|
||||
let Some(state) = &states[at] else {
|
||||
continue;
|
||||
};
|
||||
let delta = match previous_index {
|
||||
None => *target,
|
||||
Some(previous) => target
|
||||
.checked_sub(previous)
|
||||
.and_then(|delta| delta.checked_sub(1))
|
||||
.ok_or_else(|| merger.invalid("кадры не по порядку"))?,
|
||||
};
|
||||
out.push(select(delta, &state.locals, &previous_locals, &state.stack));
|
||||
previous_locals = state.locals.clone();
|
||||
previous_index = Some(*target);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn select(
|
||||
delta: u32,
|
||||
locals: &[VerificationType],
|
||||
previous: &[VerificationType],
|
||||
stack: &[VerificationType],
|
||||
) -> StackMapFrame {
|
||||
// Сравнение — на гранулярности переменных (`long` — одна запись,
|
||||
// как у `javac`): иначе половины пар дают `Append{[Top, Long]}`.
|
||||
let current = vars(locals);
|
||||
let base = vars(previous);
|
||||
if current == base {
|
||||
if stack.is_empty() {
|
||||
return StackMapFrame::Same {
|
||||
offset_delta: delta,
|
||||
};
|
||||
}
|
||||
if let [local] = stack {
|
||||
return StackMapFrame::SameLocals {
|
||||
offset_delta: delta,
|
||||
local: local.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
if stack.is_empty()
|
||||
&& let Some(added) = current.strip_prefix(base.as_slice())
|
||||
&& (1..=3).contains(&added.len())
|
||||
&& !added.contains(&VerificationType::Top)
|
||||
{
|
||||
return StackMapFrame::Append {
|
||||
offset_delta: delta,
|
||||
locals: added.to_vec(),
|
||||
};
|
||||
}
|
||||
if stack.is_empty()
|
||||
&& let Some(dropped) = base.strip_prefix(current.as_slice())
|
||||
&& (1..=3).contains(&dropped.len())
|
||||
&& let Ok(count) = u8::try_from(dropped.len())
|
||||
{
|
||||
return StackMapFrame::Chop {
|
||||
offset_delta: delta,
|
||||
count,
|
||||
};
|
||||
}
|
||||
StackMapFrame::Full {
|
||||
offset_delta: delta,
|
||||
locals: current,
|
||||
stack: stack.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Слоты → переменные: `[Long, Top]` — одна запись; хвостовые `Top` — прочь.
|
||||
///
|
||||
/// Одиночные `Top` внутри (дыры) сохраняются: в `Full` это легальный тип,
|
||||
/// а в `Append` мы их не пускаем (см. `select`).
|
||||
fn vars(locals: &[VerificationType]) -> Vec<VerificationType> {
|
||||
let mut out = Vec::with_capacity(locals.len());
|
||||
let mut index = 0;
|
||||
while index < locals.len() {
|
||||
match &locals[index] {
|
||||
VerificationType::Long | VerificationType::Double
|
||||
if locals.get(index + 1) == Some(&VerificationType::Top) =>
|
||||
{
|
||||
out.push(locals[index].clone());
|
||||
index += 2;
|
||||
}
|
||||
other => {
|
||||
out.push(other.clone());
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
while out.last() == Some(&VerificationType::Top) {
|
||||
out.pop();
|
||||
}
|
||||
out
|
||||
}
|
||||
122
crates/noxis-classfile/src/format/frames/flow.rs
Normal file
122
crates/noxis-classfile/src/format/frames/flow.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
//! CFG тела метода: метки, рёбра, точки кадров, диапазоны handler'ов.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::merge::Merger;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Insn, Label};
|
||||
use crate::model::ExceptionHandler;
|
||||
|
||||
/// Метки → номера инструкций без маркеров.
|
||||
pub(super) fn strip_marks<'a>(
|
||||
insns: &'a [Insn],
|
||||
merger: &Merger<'_>,
|
||||
) -> Result<(BTreeMap<Label, u32>, Vec<&'a Insn>), ClassfileError> {
|
||||
let mut labels = BTreeMap::new();
|
||||
let mut bare = Vec::with_capacity(insns.len());
|
||||
for insn in insns {
|
||||
match insn {
|
||||
Insn::Mark(label) => {
|
||||
let index = u32::try_from(bare.len())
|
||||
.map_err(|_| merger.invalid("слишком много инструкций"))?;
|
||||
if let Some(prev) = labels.insert(*label, index)
|
||||
&& prev != index
|
||||
{
|
||||
return Err(merger.invalid(&format!("метка {label} объявлена дважды")));
|
||||
}
|
||||
}
|
||||
_ => bare.push(insn),
|
||||
}
|
||||
}
|
||||
Ok((labels, bare))
|
||||
}
|
||||
|
||||
/// Рёбра CFG без исключений: продолжение + цели переходов.
|
||||
pub(super) fn successors(
|
||||
insn: &Insn,
|
||||
index: u32,
|
||||
len: u32,
|
||||
labels: &BTreeMap<Label, u32>,
|
||||
merger: &Merger<'_>,
|
||||
) -> Result<Vec<u32>, ClassfileError> {
|
||||
let mut out = Vec::new();
|
||||
// Без продолжения: переходы без возврата и выходы из метода.
|
||||
// `Jsr`/`Ret` сюда не доходят (их отвергает `transfer`), но рёбра есть.
|
||||
let terminal = matches!(
|
||||
insn,
|
||||
Insn::Goto(..)
|
||||
| Insn::Jsr(..)
|
||||
| Insn::Ret(..)
|
||||
| Insn::TableSwitch { .. }
|
||||
| Insn::LookupSwitch { .. }
|
||||
| Insn::Return(..)
|
||||
| Insn::AReturn
|
||||
| Insn::AThrow
|
||||
);
|
||||
if !terminal && index + 1 < len {
|
||||
out.push(index + 1);
|
||||
}
|
||||
for target in insn.targets() {
|
||||
out.push(
|
||||
labels
|
||||
.get(&target)
|
||||
.copied()
|
||||
.ok_or_else(|| merger.invalid(&format!("нет метки {target}")))?,
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Точки, где верификатор требует кадры: цели переходов + входы handler'ов.
|
||||
pub(super) fn frame_targets(
|
||||
bare: &[&Insn],
|
||||
labels: &BTreeMap<Label, u32>,
|
||||
handlers: &[HandlerRange],
|
||||
merger: &Merger<'_>,
|
||||
) -> Result<BTreeSet<u32>, ClassfileError> {
|
||||
let mut targets = BTreeSet::new();
|
||||
for insn in bare {
|
||||
for target in insn.targets() {
|
||||
targets.insert(
|
||||
labels
|
||||
.get(&target)
|
||||
.copied()
|
||||
.ok_or_else(|| merger.invalid(&format!("нет метки {target}")))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (_, _, handler, _) in handlers {
|
||||
targets.insert(*handler);
|
||||
}
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
/// Точка handler'а в номерах инструкций: `(start, end, handler, тип перехвата)`.
|
||||
///
|
||||
/// Диапазоны полуоткрытые `[start, end)`, как в JVMS.
|
||||
pub(super) type HandlerRange = (u32, u32, u32, Option<String>);
|
||||
|
||||
/// Handler'ы в номерах инструкций для рёбер исключений.
|
||||
pub(super) fn handler_ranges(
|
||||
table: &[ExceptionHandler],
|
||||
labels: &BTreeMap<Label, u32>,
|
||||
merger: &Merger<'_>,
|
||||
) -> Result<Vec<HandlerRange>, ClassfileError> {
|
||||
table
|
||||
.iter()
|
||||
.map(|handler| {
|
||||
let index_of = |label: Label| -> Result<u32, ClassfileError> {
|
||||
labels
|
||||
.get(&label)
|
||||
.copied()
|
||||
.ok_or_else(|| merger.invalid(&format!("нет метки {label}")))
|
||||
};
|
||||
Ok((
|
||||
index_of(handler.start)?,
|
||||
index_of(handler.end)?,
|
||||
index_of(handler.handler)?,
|
||||
handler.catch_type.clone(),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
158
crates/noxis-classfile/src/format/frames/merge/descs.rs
Normal file
158
crates/noxis-classfile/src/format/frames/merge/descs.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//! Разбор дескрипторов типов (`(...)V`, `[L...;`).
|
||||
|
||||
use super::super::VerificationType;
|
||||
|
||||
/// Ошибка разбора дескриптора (контекст добавляет вызывающий код).
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DescError(pub String);
|
||||
|
||||
/// Уложить слоты значения в locals с позиции; вернуть следующую позицию.
|
||||
pub(crate) fn write_local(
|
||||
locals: &mut [VerificationType],
|
||||
slot: usize,
|
||||
value: Vec<VerificationType>,
|
||||
) -> Result<usize, ()> {
|
||||
if slot + value.len() > locals.len() {
|
||||
return Err(());
|
||||
}
|
||||
let width = value.len().max(1);
|
||||
for (offset, item) in value.into_iter().enumerate() {
|
||||
locals[slot + offset] = item;
|
||||
}
|
||||
Ok(slot + width)
|
||||
}
|
||||
|
||||
/// Разобрать дескриптор массива на (глубина, элемент без `;`).
|
||||
pub(crate) fn split_array(descriptor: &str) -> Option<(usize, &str)> {
|
||||
let dims = descriptor.bytes().take_while(|&b| b == b'[').count();
|
||||
if dims == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((dims, descriptor[dims..].trim_end_matches(';')))
|
||||
}
|
||||
|
||||
/// Слоты одного полевого дескриптора (`long`/`double` — парой с `Top`).
|
||||
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
|
||||
let mut chars = descriptor.chars();
|
||||
match chars.next() {
|
||||
Some('B') | Some('C') | Some('I') | Some('S') | Some('Z') => {
|
||||
Ok(vec![VerificationType::Integer])
|
||||
}
|
||||
Some('F') => Ok(vec![VerificationType::Float]),
|
||||
Some('J') => Ok(vec![VerificationType::Long, VerificationType::Top]),
|
||||
Some('D') => Ok(vec![VerificationType::Double, VerificationType::Top]),
|
||||
Some('V') => Ok(Vec::new()),
|
||||
Some('L') => {
|
||||
let mut name = String::new();
|
||||
for char in chars {
|
||||
if char == ';' {
|
||||
return Ok(vec![VerificationType::Object(name)]);
|
||||
}
|
||||
name.push(char);
|
||||
}
|
||||
Err(DescError(format!("нет `;` в {descriptor}")))
|
||||
}
|
||||
Some('[') => Ok(vec![VerificationType::Object(descriptor.to_string())]),
|
||||
_ => Err(DescError(format!("кривой дескриптор {descriptor}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Слоты параметров и возврата (`None` — `void`).
|
||||
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
|
||||
|
||||
/// Дескриптор метода: слоты параметров и слоты возврата (`None` — `void`).
|
||||
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
|
||||
let (params, ret) = descriptor
|
||||
.split_once('(')
|
||||
.and_then(|(_, rest)| rest.split_once(')'))
|
||||
.ok_or_else(|| DescError(format!("кривой дескриптор {descriptor}")))?;
|
||||
let mut out = Vec::new();
|
||||
let mut index = 0;
|
||||
let bytes = params.as_bytes();
|
||||
while index < bytes.len() {
|
||||
match bytes[index] as char {
|
||||
'B' | 'C' | 'I' | 'S' | 'Z' => {
|
||||
out.push(vec![VerificationType::Integer]);
|
||||
index += 1;
|
||||
}
|
||||
'F' => {
|
||||
out.push(vec![VerificationType::Float]);
|
||||
index += 1;
|
||||
}
|
||||
'J' => {
|
||||
out.push(vec![VerificationType::Long, VerificationType::Top]);
|
||||
index += 1;
|
||||
}
|
||||
'D' => {
|
||||
out.push(vec![VerificationType::Double, VerificationType::Top]);
|
||||
index += 1;
|
||||
}
|
||||
'L' => {
|
||||
let end = params[index..]
|
||||
.find(';')
|
||||
.ok_or_else(|| DescError(format!("нет `;` в {descriptor}")))?;
|
||||
out.push(vec![VerificationType::Object(
|
||||
params[index + 1..index + end].to_string(),
|
||||
)]);
|
||||
index += end + 1;
|
||||
}
|
||||
'[' => {
|
||||
let mut end = index;
|
||||
while bytes[end] == b'[' {
|
||||
end += 1;
|
||||
}
|
||||
if bytes[end] == b'L' {
|
||||
let semi = params[end..]
|
||||
.find(';')
|
||||
.ok_or_else(|| DescError(format!("нет `;` в {descriptor}")))?;
|
||||
end += semi + 1;
|
||||
} else {
|
||||
end += 1;
|
||||
}
|
||||
if end > bytes.len() {
|
||||
return Err(DescError(format!("кривой дескриптор {descriptor}")));
|
||||
}
|
||||
out.push(vec![VerificationType::Object(
|
||||
params[index..end].to_string(),
|
||||
)]);
|
||||
index = end;
|
||||
}
|
||||
_ => return Err(DescError(format!("кривой дескриптор {descriptor}"))),
|
||||
}
|
||||
}
|
||||
let ret = if ret == "V" {
|
||||
None
|
||||
} else {
|
||||
Some(field_slots(ret)?)
|
||||
};
|
||||
Ok((out, ret))
|
||||
}
|
||||
|
||||
/// Тип элемента по дескриптору массива в стеке.
|
||||
pub(crate) fn array_element(
|
||||
array: &VerificationType,
|
||||
kind: crate::insn::ArrayKind,
|
||||
) -> VerificationType {
|
||||
use crate::insn::ArrayKind::{Boolean, Byte, Char, Double, Float, Int, Long, Reference, Short};
|
||||
if let VerificationType::Object(descriptor) = array
|
||||
&& let Some((dims, elem)) = split_array(descriptor)
|
||||
{
|
||||
if dims > 1 {
|
||||
return VerificationType::Object(descriptor[1..].to_string());
|
||||
}
|
||||
return match elem {
|
||||
"Z" | "B" | "C" | "S" | "I" => VerificationType::Integer,
|
||||
"J" => VerificationType::Long,
|
||||
"F" => VerificationType::Float,
|
||||
"D" => VerificationType::Double,
|
||||
_ => VerificationType::Object(format!("[{elem};")),
|
||||
};
|
||||
}
|
||||
match kind {
|
||||
Boolean | Byte | Short | Char | Int => VerificationType::Integer,
|
||||
Long => VerificationType::Long,
|
||||
Float => VerificationType::Float,
|
||||
Double => VerificationType::Double,
|
||||
Reference => VerificationType::Object("java/lang/Object".to_string()),
|
||||
}
|
||||
}
|
||||
198
crates/noxis-classfile/src/format/frames/merge/mod.rs
Normal file
198
crates/noxis-classfile/src/format/frames/merge/mod.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
//! Слияние типов (LUB) и разбор дескрипторов.
|
||||
//!
|
||||
//! Решётка: `Top` — «пусто» (берётся сосед), `Null` впитывается в ссылку,
|
||||
//! одинаковые — сами в себя, ссылки — наименьший общий предок по иерархии.
|
||||
//! Несочетаемые инициализированные типы (`int` vs ссылка после `astore`
|
||||
//! в handler'е) дают `Top`, как обрезанные кадры `javac`: `Top` в кадре
|
||||
//! принимает любое фактическое значение, поэтому это всегда sound.
|
||||
//! Исключение — неинициализированные типы: их слияние означает сломанный
|
||||
//! поток конструирования, здесь честная ошибка, а не молчаливый `Top`.
|
||||
|
||||
pub(super) mod descs;
|
||||
|
||||
use super::VerificationType;
|
||||
use super::xfer::State;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::model::Method;
|
||||
|
||||
use descs::write_local;
|
||||
|
||||
/// Иерархия классов для LUB: достаточно цепочки суперклассов.
|
||||
pub trait Hierarchy {
|
||||
/// Прямой суперкласс (`None` — `java/lang/Object` или неизвестный).
|
||||
fn superclass(&self, class: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
/// Контекст слияния: имена для ошибок + иерархия для LCA.
|
||||
pub(super) struct Merger<'h> {
|
||||
pub class: String,
|
||||
pub method: String,
|
||||
pub hierarchy: &'h dyn Hierarchy,
|
||||
}
|
||||
|
||||
impl Merger<'_> {
|
||||
pub fn invalid(&self, msg: &str) -> ClassfileError {
|
||||
ClassfileError::InvalidCode {
|
||||
class: self.class.clone(),
|
||||
msg: format!("{}: {msg}", self.method),
|
||||
}
|
||||
}
|
||||
|
||||
/// Влить входящее состояние; `true` — слот изменился.
|
||||
pub fn merge_into(
|
||||
&self,
|
||||
slot: &mut Option<State>,
|
||||
incoming: State,
|
||||
) -> Result<bool, ClassfileError> {
|
||||
match slot {
|
||||
None => {
|
||||
*slot = Some(incoming);
|
||||
Ok(true)
|
||||
}
|
||||
Some(old) => {
|
||||
let merged = self.lub_states(old, &incoming)?;
|
||||
if *old == merged {
|
||||
Ok(false)
|
||||
} else {
|
||||
*old = merged;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Начальное состояние: параметры (+ `this`) поверх `Top`.
|
||||
pub fn initial(&self, method: &Method, max_locals: u16) -> Result<State, ClassfileError> {
|
||||
let is_static = method.access_flags & 0x0008 != 0;
|
||||
let (params, _) =
|
||||
descs::method_slots(&method.descriptor).map_err(|e| self.invalid(&e.0))?;
|
||||
let mut locals = vec![VerificationType::Top; max_locals as usize];
|
||||
let mut slot = 0;
|
||||
if !is_static {
|
||||
let this = if method.name == "<init>" {
|
||||
VerificationType::UninitializedThis
|
||||
} else {
|
||||
VerificationType::Object(self.class.clone())
|
||||
};
|
||||
slot = write_local(&mut locals, slot, vec![this])
|
||||
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
|
||||
}
|
||||
for param in params {
|
||||
slot = write_local(&mut locals, slot, param)
|
||||
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
|
||||
}
|
||||
Ok(State {
|
||||
locals,
|
||||
stack: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
|
||||
if first.locals.len() != second.locals.len() {
|
||||
return Err(self.invalid("разная длина locals при слиянии"));
|
||||
}
|
||||
if first.stack.len() != second.stack.len() {
|
||||
return Err(self.invalid("разная глубина стека при слиянии"));
|
||||
}
|
||||
Ok(State {
|
||||
locals: first
|
||||
.locals
|
||||
.iter()
|
||||
.zip(&second.locals)
|
||||
.map(|(a, b)| self.lub_types(a, b))
|
||||
.collect::<Result<_, _>>()?,
|
||||
stack: first
|
||||
.stack
|
||||
.iter()
|
||||
.zip(&second.stack)
|
||||
.map(|(a, b)| self.lub_types(a, b))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn lub_types(
|
||||
&self,
|
||||
first: &VerificationType,
|
||||
second: &VerificationType,
|
||||
) -> Result<VerificationType, ClassfileError> {
|
||||
use VerificationType::{Double, Float, Integer, Long, Null, Object, Top};
|
||||
if first == second {
|
||||
return Ok(first.clone());
|
||||
}
|
||||
match (first, second) {
|
||||
(Top, other) | (other, Top) => Ok(other.clone()),
|
||||
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
|
||||
(Null, Null) => Ok(Null),
|
||||
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
|
||||
Ok(first.clone())
|
||||
}
|
||||
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
|
||||
// Одинаковые уже разобраны выше; здесь неинициализированные
|
||||
// всегда разные — это сломанный поток конструирования.
|
||||
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
|
||||
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
|
||||
Err(self.invalid("слияние неинициализированных типов"))
|
||||
}
|
||||
// Несочетаемые инициализированные (например, `int` из тела `try`
|
||||
// и `Throwable` из handler'а в одном слоте): `Top`, как у `javac`.
|
||||
_ => Ok(Top),
|
||||
}
|
||||
}
|
||||
|
||||
/// Наименьший общий предок: подъём по цепочкам, запасной — `Object`.
|
||||
fn common_superclass(&self, first: &str, second: &str) -> String {
|
||||
if first == second {
|
||||
return first.to_string();
|
||||
}
|
||||
if descs::split_array(first).is_some() || descs::split_array(second).is_some() {
|
||||
return self.array_lca(first, second);
|
||||
}
|
||||
let mut chain = Vec::new();
|
||||
let mut current = Some(first.to_string());
|
||||
let mut guard = 0;
|
||||
while let Some(name) = current {
|
||||
if name == second {
|
||||
return name;
|
||||
}
|
||||
chain.push(name.clone());
|
||||
current = self.hierarchy.superclass(&name);
|
||||
guard += 1;
|
||||
if guard > 1024 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut current = Some(second.to_string());
|
||||
guard = 0;
|
||||
while let Some(name) = current {
|
||||
if chain.contains(&name) {
|
||||
return name;
|
||||
}
|
||||
current = self.hierarchy.superclass(&name);
|
||||
guard += 1;
|
||||
if guard > 1024 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
"java/lang/Object".to_string()
|
||||
}
|
||||
|
||||
/// LCA массивов: та же размерность + LCA элементов, иначе `Object`.
|
||||
fn array_lca(&self, first: &str, second: &str) -> String {
|
||||
let (dims_a, elem_a) = descs::split_array(first).unwrap_or((0, first));
|
||||
let (dims_b, elem_b) = descs::split_array(second).unwrap_or((0, second));
|
||||
if dims_a == 0 || dims_b == 0 || dims_a != dims_b {
|
||||
return "java/lang/Object".to_string();
|
||||
}
|
||||
if elem_a == elem_b {
|
||||
return first.to_string();
|
||||
}
|
||||
if elem_a.len() == 1 || elem_b.len() == 1 {
|
||||
return "java/lang/Object".to_string();
|
||||
}
|
||||
let common = self.common_superclass(
|
||||
elem_a.strip_prefix('L').unwrap_or(elem_a),
|
||||
elem_b.strip_prefix('L').unwrap_or(elem_b),
|
||||
);
|
||||
format!("{}L{common};", "[".repeat(dims_a))
|
||||
}
|
||||
}
|
||||
235
crates/noxis-classfile/src/format/frames/mod.rs
Normal file
235
crates/noxis-classfile/src/format/frames/mod.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
//! `StackMapTable`: представление фреймов и вычисление после трансформаций.
|
||||
//!
|
||||
//! Фреймы — самая дорогая часть любого обфускатора на Rust: JVM-верификатор
|
||||
//! (class version ≥ 50) требует точного `StackMapTable`, иначе класс не грузится.
|
||||
//! Варианты реализации и их цены — `docs/stack.md`.
|
||||
//!
|
||||
//! Метод: CFG по меткам → worklist с типами (`xfer`) → слияние LUB (`merge`)
|
||||
//! → эмиссия компактных кадров (`emit`). Байтовые смещения берутся тем же
|
||||
//! преобразованием, что использует writer, — иначе кадры разъедутся с байтами.
|
||||
|
||||
mod emit;
|
||||
mod flow;
|
||||
mod merge;
|
||||
mod xfer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Insn, Label, Prim};
|
||||
use crate::model::Method;
|
||||
|
||||
use flow::{HandlerRange, frame_targets, handler_ranges, strip_marks, successors};
|
||||
pub use merge::Hierarchy;
|
||||
use merge::Merger;
|
||||
use xfer::{State, transfer};
|
||||
|
||||
/// Тип верификации из JVMS §4.10.1.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum VerificationType {
|
||||
Top,
|
||||
Integer,
|
||||
Float,
|
||||
Double,
|
||||
Long,
|
||||
Null,
|
||||
UninitializedThis,
|
||||
/// Внутреннее имя класса (`java/lang/Object`) или дескриптор массива.
|
||||
Object(String),
|
||||
/// Байтовое смещение инструкции `new`.
|
||||
Uninitialized(u16),
|
||||
}
|
||||
|
||||
impl VerificationType {
|
||||
/// Тип, соответствующий примитиву.
|
||||
pub fn of_prim(p: Prim) -> Self {
|
||||
match p {
|
||||
Prim::Int => VerificationType::Integer,
|
||||
Prim::Float => VerificationType::Float,
|
||||
Prim::Long => VerificationType::Long,
|
||||
Prim::Double => VerificationType::Double,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Запись `StackMapTable` (JVMS §4.7.4).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StackMapFrame {
|
||||
Same {
|
||||
offset_delta: u32,
|
||||
},
|
||||
SameLocals {
|
||||
offset_delta: u32,
|
||||
local: VerificationType,
|
||||
},
|
||||
Chop {
|
||||
offset_delta: u32,
|
||||
count: u8,
|
||||
},
|
||||
Append {
|
||||
offset_delta: u32,
|
||||
locals: Vec<VerificationType>,
|
||||
},
|
||||
Full {
|
||||
offset_delta: u32,
|
||||
locals: Vec<VerificationType>,
|
||||
stack: Vec<VerificationType>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Вычислить фреймы для тела метода после всех трансформаций.
|
||||
///
|
||||
/// Кадры нужны только классам версии ≥ 50; `jsr`/`ret` отвергаются (такие
|
||||
/// классы старше и кадры им не нужны). Иерархия — для LUB ссылочных типов.
|
||||
pub fn compute_frames(
|
||||
class: &str,
|
||||
method: &Method,
|
||||
hierarchy: &dyn Hierarchy,
|
||||
) -> Result<Vec<StackMapFrame>, ClassfileError> {
|
||||
let code = method
|
||||
.code
|
||||
.as_ref()
|
||||
.ok_or_else(|| invalid(class, &method.name, "нет тела"))?;
|
||||
if code.insns.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let merger = Merger {
|
||||
class: class.to_string(),
|
||||
method: method.name.clone(),
|
||||
hierarchy,
|
||||
};
|
||||
let (labels, bare) = strip_marks(&code.insns, &merger)?;
|
||||
let bytes = layout(&bare, &labels, &merger)?;
|
||||
let handlers = handler_ranges(&code.exception_table, &labels, &merger)?;
|
||||
let initial = merger.initial(method, code.max_locals)?;
|
||||
let mut states: Vec<Option<State>> = vec![None; bare.len()];
|
||||
states[0] = Some(initial.clone());
|
||||
run_worklist(&merger, &bare, &bytes, &labels, &handlers, &mut states)?;
|
||||
let targets = frame_targets(&bare, &labels, &handlers, &merger)?;
|
||||
emit::emit_frames(&states, &targets, &initial.locals, &merger)
|
||||
}
|
||||
|
||||
/// Байтовое смещение каждой инструкции — тем же кодированием, что в writer'е.
|
||||
fn layout(
|
||||
bare: &[&Insn],
|
||||
labels: &BTreeMap<Label, u32>,
|
||||
merger: &Merger<'_>,
|
||||
) -> Result<Vec<u16>, ClassfileError> {
|
||||
use crate::format::write::{code, pool::Pool};
|
||||
|
||||
let mut pool = Pool::new();
|
||||
let resolve = |label: Label| -> Result<u32, ClassfileError> {
|
||||
labels
|
||||
.get(&label)
|
||||
.copied()
|
||||
.ok_or_else(|| merger.invalid(&format!("нет метки {label}")))
|
||||
};
|
||||
let mut rt_insns = Vec::with_capacity(bare.len());
|
||||
for (index, insn) in bare.iter().enumerate() {
|
||||
let index = u32::try_from(index).map_err(|_| merger.invalid("слишком много инструкций"))?;
|
||||
if let Some(rt) = code::insns::convert(insn, index, &mut pool, &resolve)? {
|
||||
rt_insns.push(rt);
|
||||
}
|
||||
}
|
||||
let map = code::measure(&rt_insns)?;
|
||||
(0..bare.len())
|
||||
.map(|index| {
|
||||
map.get(&(index as u32))
|
||||
.copied()
|
||||
.ok_or_else(|| merger.invalid("замер потерял инструкцию"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Основной цикл: состояния текут по рёбрам CFG до неподвижной точки.
|
||||
fn run_worklist(
|
||||
merger: &Merger<'_>,
|
||||
bare: &[&Insn],
|
||||
bytes: &[u16],
|
||||
labels: &BTreeMap<Label, u32>,
|
||||
handlers: &[HandlerRange],
|
||||
states: &mut [Option<State>],
|
||||
) -> Result<(), ClassfileError> {
|
||||
let mut queue: VecDeque<u32> = VecDeque::from([0]);
|
||||
let mut fuel = 1_000_000u32;
|
||||
while let Some(index) = queue.pop_front() {
|
||||
let at = index as usize;
|
||||
let state = states[at]
|
||||
.clone()
|
||||
.ok_or_else(|| merger.invalid("пустое состояние"))?;
|
||||
let mut out = state;
|
||||
transfer(&merger.class, &merger.method, bare[at], bytes[at], &mut out).map_err(
|
||||
|error| match error {
|
||||
ClassfileError::InvalidCode { class, msg } => ClassfileError::InvalidCode {
|
||||
class,
|
||||
msg: format!("[{at}] {msg}"),
|
||||
},
|
||||
other => other,
|
||||
},
|
||||
)?;
|
||||
let nexts = successors(bare[at], index, bare.len() as u32, labels, merger)?;
|
||||
for next in nexts {
|
||||
flow_into(merger, states, &mut queue, &mut fuel, next, out.clone())?;
|
||||
}
|
||||
for (start, end, handler, catch) in handlers {
|
||||
if index >= *start && index < *end {
|
||||
let caught = State {
|
||||
locals: out.locals.clone(),
|
||||
stack: vec![catch_type(catch)],
|
||||
};
|
||||
flow_into(merger, states, &mut queue, &mut fuel, *handler, caught)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Влить состояние в инструкцию; `true` — изменилось, продолжить обход.
|
||||
fn flow_into(
|
||||
merger: &Merger<'_>,
|
||||
states: &mut [Option<State>],
|
||||
queue: &mut VecDeque<u32>,
|
||||
fuel: &mut u32,
|
||||
next: u32,
|
||||
state: State,
|
||||
) -> Result<(), ClassfileError> {
|
||||
if *fuel == 0 {
|
||||
return Err(merger.invalid("нет неподвижной точки"));
|
||||
}
|
||||
*fuel -= 1;
|
||||
let slot = states
|
||||
.get_mut(next as usize)
|
||||
.ok_or_else(|| merger.invalid("переход вне тела метода"))?;
|
||||
let changed = merger
|
||||
.merge_into(slot, state)
|
||||
.map_err(|error| match error {
|
||||
ClassfileError::InvalidCode { class, msg } => ClassfileError::InvalidCode {
|
||||
class,
|
||||
msg: format!("[->{next}] {msg}"),
|
||||
},
|
||||
other => other,
|
||||
})?;
|
||||
if changed {
|
||||
queue.push_back(next);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Рёбра CFG без исключений: продолжение + цели переходов.
|
||||
/// Тип исключения на входе в handler (`catch-all` — любое throwable).
|
||||
fn catch_type(catch: &Option<String>) -> VerificationType {
|
||||
match catch {
|
||||
Some(name) => VerificationType::Object(name.clone()),
|
||||
None => VerificationType::Object("java/lang/Throwable".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(class: &str, method: &str, msg: &str) -> ClassfileError {
|
||||
ClassfileError::InvalidCode {
|
||||
class: class.to_string(),
|
||||
msg: format!("{method}: {msg}"),
|
||||
}
|
||||
}
|
||||
179
crates/noxis-classfile/src/format/frames/tests.rs
Normal file
179
crates/noxis-classfile/src/format/frames/tests.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//! Unit-тесты `compute_frames` на синтезированном IR.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use super::{StackMapFrame, VerificationType, compute_frames};
|
||||
use crate::Hierarchy;
|
||||
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
||||
use crate::model::{Code, ExceptionHandler, Method};
|
||||
|
||||
/// Пустая иерархия: всё неизвестное — наследники `Object` (для LUB хватает).
|
||||
struct NoHierarchy;
|
||||
|
||||
impl Hierarchy for NoHierarchy {
|
||||
fn superclass(&self, _class: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn code_method(
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
max_stack: u16,
|
||||
max_locals: u16,
|
||||
insns: Vec<Insn>,
|
||||
table: Vec<ExceptionHandler>,
|
||||
) -> Method {
|
||||
Method {
|
||||
access_flags: 0x0008,
|
||||
name: name.to_string(),
|
||||
descriptor: descriptor.to_string(),
|
||||
code: Some(Code {
|
||||
max_stack,
|
||||
max_locals,
|
||||
insns,
|
||||
exception_table: table,
|
||||
frames: vec![],
|
||||
attributes: vec![],
|
||||
}),
|
||||
attributes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Ветвь → два кадра (`Same`, `SameLocals`).
|
||||
#[test]
|
||||
fn branch_emits_same_frames() {
|
||||
let mut method = code_method(
|
||||
"max",
|
||||
"(II)I",
|
||||
2,
|
||||
2,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
load(0),
|
||||
load(1),
|
||||
Insn::Branch(BranchCond::Int(Cond::Ge), 1),
|
||||
Insn::Const(Const::Int(0)),
|
||||
Insn::Goto(2),
|
||||
Insn::Mark(1),
|
||||
Insn::Const(Const::Int(1)),
|
||||
Insn::Mark(2),
|
||||
Insn::Return(Some(Prim::Int)),
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
method.access_flags = 0x0009;
|
||||
let frames = compute_frames("demo/Max", &method, &NoHierarchy).expect("frames");
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec![
|
||||
StackMapFrame::Same { offset_delta: 5 },
|
||||
StackMapFrame::SameLocals {
|
||||
offset_delta: 0,
|
||||
local: VerificationType::Integer,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// `try/catch`: handler получает `Full`, продолжение — `Append`.
|
||||
#[test]
|
||||
fn handler_emits_full_frame() {
|
||||
let method = code_method(
|
||||
"run",
|
||||
"()V",
|
||||
1,
|
||||
2,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
Insn::Const(Const::Int(0)),
|
||||
store(0),
|
||||
Insn::Mark(1),
|
||||
Insn::Const(Const::Int(1)),
|
||||
store(0),
|
||||
Insn::Goto(3),
|
||||
Insn::Mark(2),
|
||||
store_ref(1),
|
||||
Insn::Const(Const::Int(2)),
|
||||
store(0),
|
||||
Insn::Mark(3),
|
||||
Insn::Return(None),
|
||||
],
|
||||
vec![ExceptionHandler {
|
||||
start: 1,
|
||||
end: 2,
|
||||
handler: 2,
|
||||
catch_type: Some("java/lang/Exception".to_string()),
|
||||
}],
|
||||
);
|
||||
let frames = compute_frames("demo/Try", &method, &NoHierarchy).expect("frames");
|
||||
assert_eq!(
|
||||
frames,
|
||||
vec![
|
||||
StackMapFrame::Full {
|
||||
offset_delta: 5,
|
||||
locals: vec![VerificationType::Integer],
|
||||
stack: vec![VerificationType::Object("java/lang/Exception".to_string())],
|
||||
},
|
||||
StackMapFrame::Append {
|
||||
offset_delta: 2,
|
||||
locals: vec![VerificationType::Object("java/lang/Exception".to_string())],
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Два `new` на разных ветвях нельзя слить — честная ошибка, не `Top`.
|
||||
#[test]
|
||||
fn uninitialized_merge_is_rejected() {
|
||||
let method = code_method(
|
||||
"pick",
|
||||
"(Z)Ljava/lang/Object;",
|
||||
1,
|
||||
2,
|
||||
vec![
|
||||
Insn::Mark(0),
|
||||
load(0),
|
||||
Insn::Branch(BranchCond::Zero(Cond::Ne), 1),
|
||||
Insn::New("a/A".to_string()),
|
||||
store_ref(1),
|
||||
Insn::Goto(2),
|
||||
Insn::Mark(1),
|
||||
Insn::New("a/A".to_string()),
|
||||
store_ref(1),
|
||||
Insn::Mark(2),
|
||||
Insn::Load {
|
||||
slot: 1,
|
||||
kind: SlotKind::Ref,
|
||||
},
|
||||
Insn::AReturn,
|
||||
],
|
||||
vec![],
|
||||
);
|
||||
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("слияние");
|
||||
assert!(
|
||||
err.to_string().contains("неинициализированных"),
|
||||
"не тот текст: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
fn load(slot: u16) -> Insn {
|
||||
Insn::Load {
|
||||
slot,
|
||||
kind: SlotKind::Int,
|
||||
}
|
||||
}
|
||||
|
||||
fn store(slot: u16) -> Insn {
|
||||
Insn::Store {
|
||||
slot,
|
||||
kind: SlotKind::Int,
|
||||
}
|
||||
}
|
||||
|
||||
fn store_ref(slot: u16) -> Insn {
|
||||
Insn::Store {
|
||||
slot,
|
||||
kind: SlotKind::Ref,
|
||||
}
|
||||
}
|
||||
133
crates/noxis-classfile/src/format/frames/xfer/calls.rs
Normal file
133
crates/noxis-classfile/src/format/frames/xfer/calls.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//! Вызовы и константы: дескрипторы, `<init>`, слоты значений.
|
||||
|
||||
use super::super::VerificationType;
|
||||
use super::super::merge::descs::{DescError, field_slots, method_slots};
|
||||
use super::State;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Const, Convert, InvokeKind, Prim};
|
||||
|
||||
pub(super) fn const_slots(value: &Const) -> Result<Vec<VerificationType>, DescError> {
|
||||
Ok(match value {
|
||||
Const::Int(_) => vec![VerificationType::Integer],
|
||||
Const::Long(_) => vec![VerificationType::Long, VerificationType::Top],
|
||||
Const::Float(_) => vec![VerificationType::Float],
|
||||
Const::Double(_) => vec![VerificationType::Double, VerificationType::Top],
|
||||
Const::String(_) => vec![VerificationType::Object("java/lang/String".to_string())],
|
||||
Const::Class(_) => vec![VerificationType::Object("java/lang/Class".to_string())],
|
||||
Const::MethodType(_) => vec![VerificationType::Object(
|
||||
"java/lang/invoke/MethodType".to_string(),
|
||||
)],
|
||||
Const::MethodHandle { .. } => vec![VerificationType::Object(
|
||||
"java/lang/invoke/MethodHandle".to_string(),
|
||||
)],
|
||||
Const::Dynamic { descriptor, .. } => field_slots(descriptor)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn invoke(
|
||||
class: &str,
|
||||
method: &str,
|
||||
state: &mut State,
|
||||
kind: InvokeKind,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
) -> Result<(), ClassfileError> {
|
||||
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
super::stack::pop_slots(
|
||||
state,
|
||||
count_slots(¶ms),
|
||||
class,
|
||||
method,
|
||||
"аргументы вызова",
|
||||
)?;
|
||||
let mut receiver = None;
|
||||
if kind != InvokeKind::Static {
|
||||
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
|
||||
}
|
||||
// `<init>` инициализирует получателя во ВСЕХ его копиях.
|
||||
if name == "<init>" {
|
||||
match receiver {
|
||||
Some(VerificationType::UninitializedThis) => {
|
||||
replace(
|
||||
state,
|
||||
&VerificationType::UninitializedThis,
|
||||
&initialized(class),
|
||||
);
|
||||
}
|
||||
Some(VerificationType::Uninitialized(_)) => {
|
||||
let fresh = VerificationType::Object(owner.to_string());
|
||||
replace_uninit(state, &fresh);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(ret) = ret {
|
||||
super::stack::push(state, ret)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Число слотов всех параметров.
|
||||
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
|
||||
params.iter().map(Vec::len).sum()
|
||||
}
|
||||
|
||||
/// Заменить все вхождения типа новым (инициализация через `<init>`).
|
||||
fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
|
||||
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
|
||||
if slot == from {
|
||||
*slot = to.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Любой `Uninitialized` (кроме `this`) → тип владельца (созданный `new`).
|
||||
fn replace_uninit(state: &mut State, to: &VerificationType) {
|
||||
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
|
||||
if matches!(slot, VerificationType::Uninitialized(_)) {
|
||||
*slot = to.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn initialized(class: &str) -> VerificationType {
|
||||
VerificationType::Object(class.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn context(class: &str, method: &str, error: DescError) -> ClassfileError {
|
||||
super::super::invalid(class, method, &error.0)
|
||||
}
|
||||
|
||||
pub(super) fn prim_slots(prim: Prim) -> u32 {
|
||||
match prim {
|
||||
Prim::Int | Prim::Float => 1,
|
||||
Prim::Long | Prim::Double => 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn prim_push(prim: Prim) -> Vec<VerificationType> {
|
||||
match VerificationType::of_prim(prim) {
|
||||
value @ (VerificationType::Long | VerificationType::Double) => {
|
||||
vec![value, VerificationType::Top]
|
||||
}
|
||||
value => vec![value],
|
||||
}
|
||||
}
|
||||
|
||||
/// `(слоты аргумента, примитив результата)` для конверсии.
|
||||
pub(super) fn convert_shapes(op: Convert) -> (usize, Prim) {
|
||||
use Convert::{D2F, D2I, D2L, F2D, F2I, F2L, I2B, I2C, I2D, I2F, I2L, I2S, L2D, L2F, L2I};
|
||||
use Prim::{Double, Float, Int, Long};
|
||||
match op {
|
||||
I2L => (1, Long),
|
||||
I2D => (1, Double),
|
||||
I2F | I2B | I2C | I2S => (1, if op == I2F { Float } else { Int }),
|
||||
L2I | L2F => (2, if op == L2F { Float } else { Int }),
|
||||
L2D => (2, Double),
|
||||
F2I | F2L => (1, if op == F2L { Long } else { Int }),
|
||||
F2D => (1, Double),
|
||||
D2I | D2L => (2, if op == D2L { Long } else { Int }),
|
||||
D2F => (2, Float),
|
||||
}
|
||||
}
|
||||
233
crates/noxis-classfile/src/format/frames/xfer/mod.rs
Normal file
233
crates/noxis-classfile/src/format/frames/xfer/mod.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
//! Состояние типов и transfer-функции инструкций.
|
||||
//!
|
||||
//! Стек растёт вправо (`push` — в конец). `long`/`double` занимают два слота:
|
||||
//! значение + `Top`. Проверяется только ФОРМА (глубина, категории), не
|
||||
//! соответствие типов — неверные типы честно упадут на верификаторе JVM.
|
||||
|
||||
pub(super) mod calls;
|
||||
pub(super) mod stack;
|
||||
|
||||
use super::merge::descs::{array_element, field_slots, method_slots};
|
||||
use super::{VerificationType, invalid};
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Arith, ArrayKind, BranchCond, Insn, SlotKind};
|
||||
|
||||
use calls::{const_slots, context, convert_shapes, count_slots, invoke, prim_push, prim_slots};
|
||||
use stack::{dup, dup2, local_get, pop_slots, push, store_local, wide_push};
|
||||
|
||||
/// Типы locals + operand stack в одной точке программы.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub(super) struct State {
|
||||
pub locals: Vec<VerificationType>,
|
||||
pub stack: Vec<VerificationType>,
|
||||
}
|
||||
|
||||
/// Применить инструкцию к состоянию (на месте).
|
||||
pub(super) fn transfer(
|
||||
class: &str,
|
||||
method: &str,
|
||||
insn: &Insn,
|
||||
offset: u16,
|
||||
state: &mut State,
|
||||
) -> Result<(), ClassfileError> {
|
||||
match insn {
|
||||
Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))),
|
||||
Insn::Nop => Ok(()),
|
||||
Insn::AconstNull => push(state, vec![VerificationType::Null]),
|
||||
Insn::Const(value) => {
|
||||
let slots = const_slots(value).map_err(|e| context(class, method, e))?;
|
||||
push(state, slots)
|
||||
}
|
||||
Insn::Load { slot, kind } => {
|
||||
let value = local_get(state, *slot, class, method)?;
|
||||
let _ = kind;
|
||||
push(state, wide_push(value))
|
||||
}
|
||||
Insn::Store { slot, kind } => {
|
||||
let slots = usize::from(*kind == SlotKind::Long || *kind == SlotKind::Double) + 1;
|
||||
let taken = pop_slots(state, slots, class, method, "store")?;
|
||||
store_local(state, *slot, *kind, taken, class, method)
|
||||
}
|
||||
Insn::Iinc { index, .. } => {
|
||||
let current = local_get(state, *index, class, method)?;
|
||||
if current != VerificationType::Integer {
|
||||
return Err(invalid(class, method, "iinc не по int"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Insn::ArrayLoad(kind) => {
|
||||
pop_slots(state, 1, class, method, "индекс массива")?;
|
||||
let array = pop_slots(state, 1, class, method, "массив")?.remove(0);
|
||||
push(state, vec![array_element(&array, *kind)])
|
||||
}
|
||||
Insn::ArrayStore(kind) => {
|
||||
let count = match kind {
|
||||
ArrayKind::Long | ArrayKind::Double => 2,
|
||||
_ => 1,
|
||||
};
|
||||
pop_slots(state, count, class, method, "значение")?;
|
||||
pop_slots(state, 1, class, method, "индекс массива")?;
|
||||
pop_slots(state, 1, class, method, "массив")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::Pop => {
|
||||
let top = pop_slots(state, 1, class, method, "pop")?.remove(0);
|
||||
if top == VerificationType::Top {
|
||||
return Err(invalid(class, method, "pop половины long/double"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Insn::Pop2 => {
|
||||
pop_slots(state, 2, class, method, "pop2")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::Dup => dup(state, class, method, &[1], &[1, 1]),
|
||||
Insn::DupX1 => dup(state, class, method, &[2, 1], &[1, 2, 1]),
|
||||
Insn::DupX2 => dup(state, class, method, &[3, 2, 1], &[1, 3, 2, 1]),
|
||||
Insn::Dup2 => dup2(state, class, method, false, false),
|
||||
Insn::Dup2X1 => dup2(state, class, method, true, false),
|
||||
Insn::Dup2X2 => dup2(state, class, method, true, true),
|
||||
Insn::Swap => {
|
||||
let mut taken = pop_slots(state, 2, class, method, "swap")?;
|
||||
if taken.contains(&VerificationType::Top) {
|
||||
return Err(invalid(class, method, "swap с половиной"));
|
||||
}
|
||||
taken.reverse();
|
||||
push(state, taken)
|
||||
}
|
||||
Insn::Binary { prim, op } => {
|
||||
let slots = prim_slots(*prim);
|
||||
if *op == Arith::Neg {
|
||||
pop_slots(state, slots as usize, class, method, "neg")?;
|
||||
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
|
||||
// Сдвиг берёт (значение, int), а не два значения.
|
||||
pop_slots(state, slots as usize + 1, class, method, "shift")?;
|
||||
} else {
|
||||
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
|
||||
}
|
||||
push(state, prim_push(*prim))
|
||||
}
|
||||
Insn::Cmp { prim, .. } => {
|
||||
pop_slots(state, 2 * prim_slots(*prim) as usize, class, method, "cmp")?;
|
||||
push(state, vec![VerificationType::Integer])
|
||||
}
|
||||
Insn::Convert(op) => {
|
||||
let (src, dst) = convert_shapes(*op);
|
||||
pop_slots(state, src, class, method, "convert")?;
|
||||
push(state, prim_push(dst))
|
||||
}
|
||||
Insn::Branch(cond, _) => {
|
||||
let count = match cond {
|
||||
BranchCond::Zero(..) | BranchCond::Null { .. } => 1,
|
||||
BranchCond::Int(..) | BranchCond::Ref { .. } => 2,
|
||||
};
|
||||
pop_slots(state, count, class, method, "branch")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::Goto(_) => Ok(()),
|
||||
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
|
||||
class,
|
||||
method,
|
||||
"подпрограммы jsr/ret не поддерживаются",
|
||||
)),
|
||||
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
|
||||
pop_slots(state, 1, class, method, "switch")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::Return(prim) => {
|
||||
if let Some(prim) = prim {
|
||||
pop_slots(state, prim_slots(*prim) as usize, class, method, "return")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Insn::AReturn => {
|
||||
pop_slots(state, 1, class, method, "areturn")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::GetStatic { descriptor, .. } => {
|
||||
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
push(state, slots)
|
||||
}
|
||||
Insn::PutStatic { descriptor, .. } => {
|
||||
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
pop_slots(state, slots.len(), class, method, "putstatic")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::GetField { descriptor, .. } => {
|
||||
pop_slots(state, 1, class, method, "objectref")?;
|
||||
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
push(state, slots)
|
||||
}
|
||||
Insn::PutField { descriptor, .. } => {
|
||||
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
pop_slots(state, slots.len(), class, method, "putfield")?;
|
||||
pop_slots(state, 1, class, method, "objectref")?;
|
||||
Ok(())
|
||||
}
|
||||
Insn::Invoke {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
..
|
||||
} => invoke(class, method, state, *kind, owner, name, descriptor),
|
||||
Insn::InvokeDynamic { descriptor, .. } => {
|
||||
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||
pop_slots(state, count_slots(¶ms), class, method, "invokedynamic")?;
|
||||
if let Some(ret) = ret {
|
||||
push(state, ret)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Insn::New(_) => push(state, vec![VerificationType::Uninitialized(offset)]),
|
||||
Insn::NewArray(kind) => {
|
||||
pop_slots(state, 1, class, method, "newarray")?;
|
||||
let descriptor = match kind {
|
||||
ArrayKind::Boolean => "[Z",
|
||||
ArrayKind::Char => "[C",
|
||||
ArrayKind::Float => "[F",
|
||||
ArrayKind::Double => "[D",
|
||||
ArrayKind::Byte => "[B",
|
||||
ArrayKind::Short => "[S",
|
||||
ArrayKind::Int => "[I",
|
||||
ArrayKind::Long => "[J",
|
||||
ArrayKind::Reference => {
|
||||
return Err(invalid(class, method, "newarray ссылочного типа"));
|
||||
}
|
||||
};
|
||||
push(
|
||||
state,
|
||||
vec![VerificationType::Object(descriptor.to_string())],
|
||||
)
|
||||
}
|
||||
Insn::ANewArray(name) => {
|
||||
pop_slots(state, 1, class, method, "anewarray")?;
|
||||
push(state, vec![VerificationType::Object(format!("[L{name};"))])
|
||||
}
|
||||
Insn::MultiANewArray { descriptor, dims } => {
|
||||
pop_slots(state, *dims as usize, class, method, "multianewarray")?;
|
||||
push(state, vec![VerificationType::Object(descriptor.clone())])
|
||||
}
|
||||
Insn::ArrayLength => {
|
||||
pop_slots(state, 1, class, method, "arraylength")?;
|
||||
push(state, vec![VerificationType::Integer])
|
||||
}
|
||||
Insn::AThrow => {
|
||||
pop_slots(state, 1, class, method, "athrow")?;
|
||||
state.stack.clear();
|
||||
Ok(())
|
||||
}
|
||||
Insn::CheckCast(name) => {
|
||||
pop_slots(state, 1, class, method, "checkcast")?;
|
||||
push(state, vec![VerificationType::Object(name.clone())])
|
||||
}
|
||||
Insn::InstanceOf(_) => {
|
||||
pop_slots(state, 1, class, method, "instanceof")?;
|
||||
push(state, vec![VerificationType::Integer])
|
||||
}
|
||||
Insn::MonitorEnter | Insn::MonitorExit => {
|
||||
pop_slots(state, 1, class, method, "monitor")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
161
crates/noxis-classfile/src/format/frames/xfer/stack.rs
Normal file
161
crates/noxis-classfile/src/format/frames/xfer/stack.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
//! Стек и locals: снятие/укладка слотов, `dup*`.
|
||||
|
||||
use super::super::{VerificationType, invalid};
|
||||
use super::State;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::SlotKind;
|
||||
|
||||
/// Запись в local с учётом ширины (`long`/`double` — парой с `Top`).
|
||||
pub(super) fn store_local(
|
||||
state: &mut State,
|
||||
slot: u16,
|
||||
kind: SlotKind,
|
||||
taken: Vec<VerificationType>,
|
||||
class: &str,
|
||||
method: &str,
|
||||
) -> Result<(), ClassfileError> {
|
||||
let wide = kind == SlotKind::Long || kind == SlotKind::Double;
|
||||
if taken.len() != usize::from(wide) + 1 {
|
||||
return Err(invalid(class, method, "ширина store не совпала"));
|
||||
}
|
||||
let mut taken = taken;
|
||||
local_set(state, slot, taken.remove(0), class, method)?;
|
||||
if wide {
|
||||
local_set(
|
||||
state,
|
||||
slot.checked_add(1)
|
||||
.ok_or_else(|| invalid(class, method, "слот вне u16"))?,
|
||||
VerificationType::Top,
|
||||
class,
|
||||
method,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn push(state: &mut State, slots: Vec<VerificationType>) -> Result<(), ClassfileError> {
|
||||
state.stack.extend(slots);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Снять `n` слотов (снизу вверх); нехватка — честная ошибка.
|
||||
pub(super) fn pop_slots(
|
||||
state: &mut State,
|
||||
n: usize,
|
||||
class: &str,
|
||||
method: &str,
|
||||
what: &str,
|
||||
) -> Result<Vec<VerificationType>, ClassfileError> {
|
||||
if state.stack.len() < n {
|
||||
return Err(invalid(
|
||||
class,
|
||||
method,
|
||||
&format!("переполнение стека вниз в {what}"),
|
||||
));
|
||||
}
|
||||
Ok(state.stack.split_off(state.stack.len() - n))
|
||||
}
|
||||
|
||||
pub(super) fn local_get(
|
||||
state: &State,
|
||||
slot: u16,
|
||||
class: &str,
|
||||
method: &str,
|
||||
) -> Result<VerificationType, ClassfileError> {
|
||||
state
|
||||
.locals
|
||||
.get(slot as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| invalid(class, method, &format!("чтение слота {slot} вне locals")))
|
||||
}
|
||||
|
||||
pub(super) fn local_set(
|
||||
state: &mut State,
|
||||
slot: u16,
|
||||
value: VerificationType,
|
||||
class: &str,
|
||||
method: &str,
|
||||
) -> Result<(), ClassfileError> {
|
||||
let cell = state
|
||||
.locals
|
||||
.get_mut(slot as usize)
|
||||
.ok_or_else(|| invalid(class, method, &format!("запись слота {slot} вне locals")))?;
|
||||
*cell = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Значение local'а целиком: `long`/`double` — парой с `Top`.
|
||||
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
|
||||
match value {
|
||||
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
|
||||
_ => vec![value],
|
||||
}
|
||||
}
|
||||
|
||||
/// `dup*` над значениями-категории-1: снять по позициям `take`, вернуть `put`.
|
||||
pub(super) fn dup(
|
||||
state: &mut State,
|
||||
class: &str,
|
||||
method: &str,
|
||||
take: &[usize],
|
||||
put: &[usize],
|
||||
) -> Result<(), ClassfileError> {
|
||||
let depth = take.iter().max().copied().unwrap_or(0);
|
||||
let taken = pop_slots(state, depth, class, method, "dup")?;
|
||||
if taken.contains(&VerificationType::Top) {
|
||||
return Err(invalid(class, method, "dup над половиной"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(put.len());
|
||||
for position in put {
|
||||
out.push(taken[depth - position].clone());
|
||||
}
|
||||
push(state, out)
|
||||
}
|
||||
|
||||
/// `dup2*`: форма выбирается по вершине (`Top` — вторая половина `long`).
|
||||
pub(super) fn dup2(
|
||||
state: &mut State,
|
||||
class: &str,
|
||||
method: &str,
|
||||
deep: bool,
|
||||
deeper: bool,
|
||||
) -> Result<(), ClassfileError> {
|
||||
let wide = state.stack.last() == Some(&VerificationType::Top);
|
||||
if wide {
|
||||
let depth = if deeper {
|
||||
4
|
||||
} else if deep {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let taken = pop_slots(state, depth, class, method, "dup2")?;
|
||||
if taken[depth - 1] != VerificationType::Top {
|
||||
return Err(invalid(class, method, "dup2: нет половины"));
|
||||
}
|
||||
let value = taken[depth - 2].clone();
|
||||
if !matches!(value, VerificationType::Long | VerificationType::Double) {
|
||||
return Err(invalid(class, method, "dup2: половина без long/double"));
|
||||
}
|
||||
let mut out = vec![value, VerificationType::Top];
|
||||
out.extend(taken);
|
||||
push(state, out)
|
||||
} else {
|
||||
let depth = if deeper {
|
||||
4
|
||||
} else if deep {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let taken = pop_slots(state, depth, class, method, "dup2")?;
|
||||
if taken.contains(&VerificationType::Top) {
|
||||
return Err(invalid(class, method, "dup2 над половиной"));
|
||||
}
|
||||
let (head, tail) = taken.split_at(depth - 2);
|
||||
let mut out = tail.to_vec();
|
||||
out.extend_from_slice(head);
|
||||
out.extend_from_slice(tail);
|
||||
push(state, out)
|
||||
}
|
||||
}
|
||||
29
crates/noxis-classfile/src/format/mod.rs
Normal file
29
crates/noxis-classfile/src/format/mod.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! Чтение/запись бинарного формата `.class` (JVMS §4) через backend
|
||||
//! `ristretto_classfile`: обзор кандидатов и решение — `docs/stack.md`.
|
||||
|
||||
mod frames;
|
||||
mod parse;
|
||||
pub(crate) mod write;
|
||||
|
||||
pub use frames::{Hierarchy, StackMapFrame, VerificationType, compute_frames};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::model::ClassFile;
|
||||
|
||||
/// Разобрать один `.class` в [`ClassFile`].
|
||||
pub fn parse(bytes: &[u8]) -> Result<ClassFile, ClassfileError> {
|
||||
parse::class::parse_class(bytes)
|
||||
}
|
||||
|
||||
/// Записать [`ClassFile`] в байты, включая пересчёт `Code`/`StackMapTable`.
|
||||
pub fn write(class: &ClassFile) -> Result<Vec<u8>, ClassfileError> {
|
||||
write::write_class(class)
|
||||
}
|
||||
|
||||
/// Классы, которые не изменялись, должны уезжать в выходной архив «как есть».
|
||||
///
|
||||
/// Это дешевле и безопаснее, чем round-trip через наш IR: нет риска потерять
|
||||
/// неизвестный атрибут. Реализация зависит от решения по backend (P1).
|
||||
pub fn passthrough_needed(class: &ClassFile) -> bool {
|
||||
class.methods.is_empty() && class.fields.is_empty()
|
||||
}
|
||||
204
crates/noxis-classfile/src/format/parse/attrs.rs
Normal file
204
crates/noxis-classfile/src/format/parse/attrs.rs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
//! Атрибуты backend'а → атрибуты нашего IR.
|
||||
//!
|
||||
//! `BootstrapMethods`, `EnclosingMethod`, `MethodParameters` и `ConstantValue`
|
||||
//! моделируются явно (в них лежат ссылки пула, переживающие пересборку);
|
||||
//! остальное структурированное, но нам ненужное, уезжает в `Raw` как есть.
|
||||
//! `Code` и `StackMapTable` здесь не обрабатываются — их разбирает вызывающий
|
||||
//! код метода (см. `super::class`).
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::Attribute as RtAttr;
|
||||
|
||||
use super::pool::{
|
||||
class_name, const_from, is_interface, kind_code, name_and_type, ref_parts, utf8,
|
||||
};
|
||||
use crate::error::ClassfileError;
|
||||
use crate::model::{Attribute, BootstrapMethod, InnerClass, MethodParameter, RecordComponent};
|
||||
|
||||
/// Один атрибут backend'а → наш атрибут (кроме `Code`/`StackMapTable`).
|
||||
pub(super) fn convert(attr: &RtAttr, pool: &ConstantPool<'_>) -> Result<Attribute, ClassfileError> {
|
||||
Ok(match attr {
|
||||
RtAttr::SourceFile {
|
||||
source_file_index, ..
|
||||
} => Attribute::SourceFile(utf8(pool, *source_file_index)?),
|
||||
RtAttr::Signature {
|
||||
signature_index, ..
|
||||
} => Attribute::Signature(utf8(pool, *signature_index)?),
|
||||
RtAttr::Exceptions {
|
||||
exception_indexes, ..
|
||||
} => Attribute::Exceptions(
|
||||
exception_indexes
|
||||
.iter()
|
||||
.map(|index| class_name(pool, *index))
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
RtAttr::InnerClasses { classes, .. } => Attribute::InnerClasses(
|
||||
classes
|
||||
.iter()
|
||||
.map(|class| inner(pool, class))
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
RtAttr::EnclosingMethod {
|
||||
class_index,
|
||||
method_index,
|
||||
..
|
||||
} => Attribute::EnclosingMethod {
|
||||
class: class_name(pool, *class_index)?,
|
||||
method: enclosing_method(pool, *method_index)?,
|
||||
},
|
||||
RtAttr::MethodParameters { parameters, .. } => Attribute::MethodParameters(
|
||||
parameters
|
||||
.iter()
|
||||
.map(|param| method_param(pool, param))
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
RtAttr::BootstrapMethods { methods, .. } => Attribute::BootstrapMethods(
|
||||
methods
|
||||
.iter()
|
||||
.map(|method| bootstrap(pool, method))
|
||||
.collect::<Result<_, _>>()?,
|
||||
),
|
||||
RtAttr::ConstantValue {
|
||||
constant_value_index,
|
||||
..
|
||||
} => Attribute::ConstantValue(const_from(pool, *constant_value_index)?),
|
||||
RtAttr::Deprecated { .. } => Attribute::Deprecated,
|
||||
RtAttr::Synthetic { .. } => Attribute::Synthetic,
|
||||
RtAttr::LineNumberTable { .. }
|
||||
| RtAttr::LocalVariableTable { .. }
|
||||
| RtAttr::LocalVariableTypeTable { .. } => {
|
||||
return Err(ClassfileError::Parse(
|
||||
"таблицы кода разбирает вызывающий код метода".into(),
|
||||
));
|
||||
}
|
||||
RtAttr::Record { records, .. } => Attribute::Record(
|
||||
records
|
||||
.iter()
|
||||
.map(|record| {
|
||||
Ok(RecordComponent {
|
||||
name: utf8(pool, record.name_index)?,
|
||||
descriptor: utf8(pool, record.descriptor_index)?,
|
||||
attributes: record
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attr| convert(attr, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
),
|
||||
RtAttr::Code { .. } | RtAttr::StackMapTable { .. } => {
|
||||
return Err(ClassfileError::Parse(
|
||||
"Code/StackMapTable разбирает вызывающий код метода".into(),
|
||||
));
|
||||
}
|
||||
other => raw(other)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Несмоделированный атрибут → `Raw`: имя + байты `info` (без заголовка).
|
||||
pub(super) fn raw(attr: &RtAttr) -> Result<Attribute, ClassfileError> {
|
||||
let mut bytes = Vec::new();
|
||||
attr.to_bytes(&mut bytes)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let info = bytes
|
||||
.get(6..)
|
||||
.ok_or_else(|| ClassfileError::Parse("короткая запись атрибута".into()))?;
|
||||
Ok(Attribute::Raw {
|
||||
name: attr.name().to_string(),
|
||||
data: info.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn inner(
|
||||
pool: &ConstantPool<'_>,
|
||||
class: &ristretto_classfile::attributes::InnerClass,
|
||||
) -> Result<InnerClass, ClassfileError> {
|
||||
Ok(InnerClass {
|
||||
inner: class_name(pool, class.class_info_index)?,
|
||||
outer: optional_class(pool, class.outer_class_info_index)?,
|
||||
inner_name: optional_utf8(pool, class.name_index)?,
|
||||
access_flags: class.access_flags.bits(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Метод объемлющего класса (`0` — только класс, без метода).
|
||||
fn enclosing_method(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
) -> Result<Option<(String, String)>, ClassfileError> {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(name_and_type(pool, index)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn method_param(
|
||||
pool: &ConstantPool<'_>,
|
||||
param: &ristretto_classfile::attributes::MethodParameter,
|
||||
) -> Result<MethodParameter, ClassfileError> {
|
||||
Ok(MethodParameter {
|
||||
name: optional_utf8(pool, param.name_index)?,
|
||||
access_flags: param.access_flags.bits(),
|
||||
})
|
||||
}
|
||||
|
||||
fn bootstrap(
|
||||
pool: &ConstantPool<'_>,
|
||||
method: &ristretto_classfile::attributes::BootstrapMethod,
|
||||
) -> Result<BootstrapMethod, ClassfileError> {
|
||||
let constant = pool
|
||||
.try_get(method.bootstrap_method_ref)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let (kind, owner, name, descriptor, itf) = match constant {
|
||||
ristretto_classfile::Constant::MethodHandle {
|
||||
reference_kind,
|
||||
reference_index,
|
||||
} => {
|
||||
let (owner, name, descriptor) = ref_parts(pool, *reference_index)?;
|
||||
(
|
||||
kind_code(reference_kind),
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
is_interface(pool, *reference_index),
|
||||
)
|
||||
}
|
||||
other => {
|
||||
return Err(ClassfileError::Parse(format!(
|
||||
"bootstrap-ссылка обязана быть MethodHandle, найден {other:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(BootstrapMethod {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
itf,
|
||||
args: method
|
||||
.arguments
|
||||
.iter()
|
||||
.map(|index| const_from(pool, *index))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Индекс пула, где `0` означает отсутствие значения.
|
||||
fn optional_class(pool: &ConstantPool<'_>, index: u16) -> Result<Option<String>, ClassfileError> {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(class_name(pool, index)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Индекс `CONSTANT_Utf8`, где `0` означает отсутствие значения.
|
||||
fn optional_utf8(pool: &ConstantPool<'_>, index: u16) -> Result<Option<String>, ClassfileError> {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(utf8(pool, index)?))
|
||||
}
|
||||
}
|
||||
109
crates/noxis-classfile/src/format/parse/class.rs
Normal file
109
crates/noxis-classfile/src/format/parse/class.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//! Класс целиком: backend `ClassFile` → наш [`ClassFile`](crate::model::ClassFile).
|
||||
|
||||
use ristretto_classfile::attributes::Attribute as RtAttr;
|
||||
use ristretto_classfile::{
|
||||
ClassFile as RtClass, ConstantPool, Field as RtField, Method as RtMethod,
|
||||
};
|
||||
|
||||
use super::attrs;
|
||||
use super::code::convert_code;
|
||||
use super::pool::{class_name, utf8};
|
||||
use crate::error::ClassfileError;
|
||||
use crate::model::{Attribute, ClassFile, Code, Field, Method, Version};
|
||||
|
||||
/// Разобрать байты `.class` в наш IR.
|
||||
pub(crate) fn parse_class(bytes: &[u8]) -> Result<ClassFile, ClassfileError> {
|
||||
let rt = RtClass::from_bytes(bytes).map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let pool = &rt.constant_pool;
|
||||
let interfaces = rt
|
||||
.interfaces
|
||||
.iter()
|
||||
.map(|index| class_name(pool, *index))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(ClassFile {
|
||||
version: Version {
|
||||
minor: rt.version.minor(),
|
||||
major: rt.version.major(),
|
||||
},
|
||||
access_flags: rt.access_flags.bits(),
|
||||
this_class: class_name(pool, rt.this_class)?,
|
||||
super_class: optional_class(pool, rt.super_class)?,
|
||||
interfaces,
|
||||
fields: rt
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| convert_field(field, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
methods: rt
|
||||
.methods
|
||||
.iter()
|
||||
.map(|method| convert_method(method, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
attributes: convert_attrs(&rt.attributes, pool)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_field(field: &RtField, pool: &ConstantPool<'_>) -> Result<Field, ClassfileError> {
|
||||
Ok(Field {
|
||||
access_flags: field.access_flags.bits(),
|
||||
name: utf8(pool, field.name_index)?,
|
||||
descriptor: utf8(pool, field.descriptor_index)?,
|
||||
attributes: convert_attrs(&field.attributes, pool)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_method(method: &RtMethod, pool: &ConstantPool<'_>) -> Result<Method, ClassfileError> {
|
||||
let mut code: Option<Code> = None;
|
||||
let mut rest: Vec<Attribute> = Vec::new();
|
||||
for attr in &method.attributes {
|
||||
match attr {
|
||||
RtAttr::Code {
|
||||
max_stack,
|
||||
max_locals,
|
||||
code: insns,
|
||||
exception_table,
|
||||
attributes,
|
||||
..
|
||||
} => {
|
||||
if code.is_some() {
|
||||
return Err(ClassfileError::Parse("два Code в одном методе".into()));
|
||||
}
|
||||
code = Some(convert_code(
|
||||
*max_stack,
|
||||
*max_locals,
|
||||
insns,
|
||||
exception_table,
|
||||
attributes,
|
||||
pool,
|
||||
)?);
|
||||
}
|
||||
other => rest.push(attrs::convert(other, pool)?),
|
||||
}
|
||||
}
|
||||
Ok(Method {
|
||||
access_flags: method.access_flags.bits(),
|
||||
name: utf8(pool, method.name_index)?,
|
||||
descriptor: utf8(pool, method.descriptor_index)?,
|
||||
code,
|
||||
attributes: rest,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_attrs(
|
||||
attrs: &[RtAttr],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Vec<Attribute>, ClassfileError> {
|
||||
attrs
|
||||
.iter()
|
||||
.map(|attr| attrs::convert(attr, pool))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Индекс суперкласса, где `0` — его отсутствие (`java/lang/Object`).
|
||||
fn optional_class(pool: &ConstantPool<'_>, index: u16) -> Result<Option<String>, ClassfileError> {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(class_name(pool, index)?))
|
||||
}
|
||||
}
|
||||
112
crates/noxis-classfile/src/format/parse/code/frames.rs
Normal file
112
crates/noxis-classfile/src/format/parse/code/frames.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Кадры `StackMapTable` при чтении: вариант в вариант, дельты как есть.
|
||||
//!
|
||||
//! Backend хранит кадры в номерах инструкций (см. `from_bytes_code_attributes`),
|
||||
//! наш IR — тоже: writer сам пересчитает их в байты при `to_bytes`.
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::{StackFrame, VerificationType as RtType};
|
||||
|
||||
use super::super::pool::class_name;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::format::{StackMapFrame, VerificationType};
|
||||
|
||||
/// Кадры backend'а → наши (индексные дельты — как есть).
|
||||
pub(super) fn to_frames(
|
||||
frames: &[StackFrame],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Vec<StackMapFrame>, ClassfileError> {
|
||||
frames.iter().map(|frame| to_frame(frame, pool)).collect()
|
||||
}
|
||||
|
||||
fn to_frame(frame: &StackFrame, pool: &ConstantPool<'_>) -> Result<StackMapFrame, ClassfileError> {
|
||||
Ok(match frame {
|
||||
StackFrame::SameFrame { frame_type } => StackMapFrame::Same {
|
||||
offset_delta: u32::from(*frame_type),
|
||||
},
|
||||
StackFrame::SameFrameExtended { offset_delta, .. } => StackMapFrame::Same {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
},
|
||||
StackFrame::SameLocals1StackItemFrame { frame_type, stack } => StackMapFrame::SameLocals {
|
||||
offset_delta: u32::from(*frame_type) - 64,
|
||||
local: single(stack, pool)?,
|
||||
},
|
||||
StackFrame::SameLocals1StackItemFrameExtended {
|
||||
offset_delta,
|
||||
stack,
|
||||
..
|
||||
} => StackMapFrame::SameLocals {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
local: single(stack, pool)?,
|
||||
},
|
||||
StackFrame::ChopFrame {
|
||||
frame_type,
|
||||
offset_delta,
|
||||
} => StackMapFrame::Chop {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
count: 251u8.wrapping_sub(*frame_type),
|
||||
},
|
||||
StackFrame::AppendFrame {
|
||||
offset_delta,
|
||||
locals,
|
||||
..
|
||||
} => StackMapFrame::Append {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
locals: types(locals, pool)?,
|
||||
},
|
||||
StackFrame::FullFrame {
|
||||
offset_delta,
|
||||
locals,
|
||||
stack,
|
||||
..
|
||||
} => StackMapFrame::Full {
|
||||
offset_delta: u32::from(*offset_delta),
|
||||
locals: types(locals, pool)?,
|
||||
stack: types(stack, pool)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Единственный элемент стека кадра (`same_locals_1_stack_item`).
|
||||
fn single(stack: &[RtType], pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
|
||||
let mut types = types(stack, pool)?.into_iter();
|
||||
match (types.next(), types.next()) {
|
||||
(Some(first), None) => Ok(first),
|
||||
_ => Err(ClassfileError::Parse(
|
||||
"в same_locals-кадре не один элемент стека".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn types(
|
||||
values: &[RtType],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Vec<VerificationType>, ClassfileError> {
|
||||
values.iter().map(|value| vtype(value, pool)).collect()
|
||||
}
|
||||
|
||||
/// Тип верификации backend'а → наш тип (имя класса берётся из constant pool).
|
||||
fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
|
||||
Ok(match value {
|
||||
RtType::Top => VerificationType::Top,
|
||||
RtType::Integer => VerificationType::Integer,
|
||||
RtType::Float => VerificationType::Float,
|
||||
RtType::Double => VerificationType::Double,
|
||||
RtType::Long => VerificationType::Long,
|
||||
RtType::Null => VerificationType::Null,
|
||||
RtType::UninitializedThis => VerificationType::UninitializedThis,
|
||||
RtType::Object { cpool_index } => VerificationType::Object(class_name(pool, *cpool_index)?),
|
||||
RtType::Uninitialized { offset } => VerificationType::Uninitialized(*offset),
|
||||
})
|
||||
}
|
||||
|
||||
/// Тип перехвата (`0` — catch-all для `finally`/`any`).
|
||||
pub(super) fn catch_type(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
) -> Result<Option<String>, ClassfileError> {
|
||||
if index == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(class_name(pool, index)?))
|
||||
}
|
||||
}
|
||||
187
crates/noxis-classfile/src/format/parse/code/labels.rs
Normal file
187
crates/noxis-classfile/src/format/parse/code/labels.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
//! Метки тела метода: где обязаны стоять и как резолвятся.
|
||||
//!
|
||||
//! Backend нумерует инструкции по порядку — метки ставятся прямо на номера,
|
||||
//! без байтовых смещений.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ristretto_classfile::attributes::Attribute as RtAttr;
|
||||
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::Label;
|
||||
|
||||
/// Номера инструкций, на которых обязана стоять метка.
|
||||
///
|
||||
/// `extra` — позиции отладочных таблиц (уже номерами): вставки пассов
|
||||
/// не должны оставлять их без меток.
|
||||
pub(super) fn label_indexes(
|
||||
code: &[Instruction],
|
||||
exception_table: &[ExceptionTableEntry],
|
||||
extra: &BTreeSet<u32>,
|
||||
) -> Result<BTreeSet<u32>, ClassfileError> {
|
||||
let mut indexes = BTreeSet::from([0u32]);
|
||||
for (position, instruction) in code.iter().enumerate() {
|
||||
let index = to_index(position)?;
|
||||
for target in branch_indexes(instruction, index) {
|
||||
indexes.insert(to_index_checked(target)?);
|
||||
}
|
||||
}
|
||||
for entry in exception_table {
|
||||
indexes.insert(u32::from(entry.range_pc.start));
|
||||
indexes.insert(u32::from(entry.range_pc.end));
|
||||
indexes.insert(u32::from(entry.handler_pc));
|
||||
}
|
||||
indexes.extend(extra.iter().copied());
|
||||
Ok(indexes)
|
||||
}
|
||||
|
||||
/// Цели ветвлений backend'а: для `if*`/`goto` — абсолютный номер, для `switch` — дельта.
|
||||
fn branch_indexes(instruction: &Instruction, index: u16) -> Vec<i32> {
|
||||
let base = i32::from(index);
|
||||
match instruction {
|
||||
Instruction::Tableswitch(table) => once(base + table.default)
|
||||
.chain(table.offsets.iter().map(|target| base + target))
|
||||
.collect(),
|
||||
Instruction::Lookupswitch(lookup) => once(base + lookup.default)
|
||||
.chain(lookup.pairs.values().map(|target| base + target))
|
||||
.collect(),
|
||||
Instruction::Ifeq(target)
|
||||
| Instruction::Ifne(target)
|
||||
| Instruction::Iflt(target)
|
||||
| Instruction::Ifge(target)
|
||||
| Instruction::Ifgt(target)
|
||||
| Instruction::Ifle(target)
|
||||
| Instruction::If_icmpeq(target)
|
||||
| Instruction::If_icmpne(target)
|
||||
| Instruction::If_icmplt(target)
|
||||
| Instruction::If_icmpge(target)
|
||||
| Instruction::If_icmpgt(target)
|
||||
| Instruction::If_icmple(target)
|
||||
| Instruction::If_acmpeq(target)
|
||||
| Instruction::If_acmpne(target)
|
||||
| Instruction::Ifnull(target)
|
||||
| Instruction::Ifnonnull(target)
|
||||
| Instruction::Goto(target)
|
||||
| Instruction::Jsr(target) => vec![i32::from(*target)],
|
||||
Instruction::Goto_w(target) | Instruction::Jsr_w(target) => vec![*target],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn once(value: i32) -> std::iter::Once<i32> {
|
||||
std::iter::once(value)
|
||||
}
|
||||
|
||||
/// Превратить отсортированный набор номеров в последовательные метки.
|
||||
pub(super) fn assign(indexes: &BTreeSet<u32>) -> BTreeMap<u32, Label> {
|
||||
indexes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(position, index)| (*index, position as u32))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Метка для номера инструкции.
|
||||
pub(super) fn lookup(labels: &BTreeMap<u32, Label>, index: u16) -> Result<Label, ClassfileError> {
|
||||
labels
|
||||
.get(&u32::from(index))
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для инструкции {index}")))
|
||||
}
|
||||
|
||||
/// Позиции отладочных таблиц, которым нужны метки.
|
||||
pub(super) fn collect_debug_labels(
|
||||
attributes: &[RtAttr],
|
||||
starts: &[u16],
|
||||
code_length: u16,
|
||||
extra: &mut BTreeSet<u32>,
|
||||
) -> Result<(), ClassfileError> {
|
||||
for attr in attributes {
|
||||
match attr {
|
||||
RtAttr::LineNumberTable { line_numbers, .. } => {
|
||||
for line in line_numbers {
|
||||
extra.insert(u32::from(line.start_pc));
|
||||
}
|
||||
}
|
||||
RtAttr::LocalVariableTable { variables, .. } => {
|
||||
for var in variables {
|
||||
extra.insert(byte_to_index(starts, var.start_pc)?);
|
||||
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
|
||||
}
|
||||
}
|
||||
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
|
||||
for var in variable_types {
|
||||
extra.insert(byte_to_index(starts, var.start_pc)?);
|
||||
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Байт → номер инструкции (только границы инструкций).
|
||||
pub(super) fn byte_to_index(starts: &[u16], byte: u16) -> Result<u32, ClassfileError> {
|
||||
starts
|
||||
.iter()
|
||||
.position(|&start| start == byte)
|
||||
.and_then(|position| u32::try_from(position).ok())
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("смещение {byte} мимо инструкции")))
|
||||
}
|
||||
|
||||
/// Конец диапазона `[start, start+length)`: индекс за концом.
|
||||
fn end_index(
|
||||
starts: &[u16],
|
||||
start: u16,
|
||||
length: u16,
|
||||
code_length: u16,
|
||||
) -> Result<u32, ClassfileError> {
|
||||
let end = start
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| ClassfileError::Parse("диапазон вне метода".into()))?;
|
||||
if end == code_length {
|
||||
return u32::try_from(starts.len())
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()));
|
||||
}
|
||||
byte_to_index(starts, end)
|
||||
}
|
||||
|
||||
/// Метка для байтового смещения отладочной таблицы.
|
||||
pub(super) fn resolve_byte(
|
||||
labels: &BTreeMap<u32, Label>,
|
||||
starts: &[u16],
|
||||
byte: u16,
|
||||
) -> Result<Label, ClassfileError> {
|
||||
let index = byte_to_index(starts, byte)?;
|
||||
labels
|
||||
.get(&index)
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для смещения {byte}")))
|
||||
}
|
||||
|
||||
/// Метка конца диапазона отладочной таблицы.
|
||||
pub(super) fn resolve_end(
|
||||
labels: &BTreeMap<u32, Label>,
|
||||
starts: &[u16],
|
||||
start: u16,
|
||||
length: u16,
|
||||
code_length: u16,
|
||||
) -> Result<Label, ClassfileError> {
|
||||
let index = end_index(starts, start, length, code_length)?;
|
||||
labels
|
||||
.get(&index)
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Parse(format!("нет метки конца {index}")))
|
||||
}
|
||||
|
||||
fn to_index(position: usize) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(position)
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))
|
||||
}
|
||||
|
||||
fn to_index_checked(index: i32) -> Result<u32, ClassfileError> {
|
||||
u32::try_from(index)
|
||||
.map_err(|_| ClassfileError::Parse(format!("номер инструкции {index} вне тела метода")))
|
||||
}
|
||||
169
crates/noxis-classfile/src/format/parse/code/mod.rs
Normal file
169
crates/noxis-classfile/src/format/parse/code/mod.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//! `Code` backend'а → тело метода нашего IR: метки, исключения, кадры.
|
||||
//!
|
||||
//! Backend уже перевёл байтовые смещения в номера инструкций — конвертеры
|
||||
//! работают прямо с ними: метки ставятся на целях переходов, границах
|
||||
//! try-блоков и позициях отладочных таблиц; кадры переносятся как есть
|
||||
//! (индексные дельты). Позиции `LineNumberTable`/`LocalVariableTable` —
|
||||
//! тоже метки, поэтому вставки пассов их не ломают (байты досчитает writer).
|
||||
|
||||
mod frames;
|
||||
mod labels;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::Cursor;
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::Attribute as RtAttr;
|
||||
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
|
||||
|
||||
use super::attrs;
|
||||
use super::insns::convert;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::Insn;
|
||||
use crate::model::{
|
||||
Attribute, Code, ExceptionHandler, LineNumber, LocalVariable, LocalVariableType,
|
||||
};
|
||||
|
||||
use frames::{catch_type, to_frames};
|
||||
use labels::{assign, collect_debug_labels, label_indexes, lookup, resolve_byte, resolve_end};
|
||||
|
||||
/// Тело метода целиком: backend `Code` → наш [`Code`].
|
||||
pub(super) fn convert_code(
|
||||
max_stack: u16,
|
||||
max_locals: u16,
|
||||
code: &[Instruction],
|
||||
exception_table: &[ExceptionTableEntry],
|
||||
attributes: &[RtAttr],
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Code, ClassfileError> {
|
||||
let (starts, code_length) = layout(code)?;
|
||||
let mut extra = BTreeSet::new();
|
||||
collect_debug_labels(attributes, &starts, code_length, &mut extra)?;
|
||||
let labels = assign(&label_indexes(code, exception_table, &extra)?);
|
||||
let resolve = |index: u16| lookup(&labels, index);
|
||||
|
||||
let mut insns = Vec::with_capacity(code.len() + labels.len());
|
||||
for (position, instruction) in code.iter().enumerate() {
|
||||
let index = u16::try_from(position)
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
|
||||
if let Some(label) = labels.get(&u32::from(index)) {
|
||||
insns.push(Insn::Mark(*label));
|
||||
}
|
||||
insns.push(convert(instruction, index, pool, &resolve)?);
|
||||
}
|
||||
let end = u32::try_from(code.len())
|
||||
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
|
||||
if let Some(label) = labels.get(&end) {
|
||||
insns.push(Insn::Mark(*label));
|
||||
}
|
||||
|
||||
let exception_table = exception_table
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
Ok(ExceptionHandler {
|
||||
start: lookup(&labels, entry.range_pc.start)?,
|
||||
end: lookup(&labels, entry.range_pc.end)?,
|
||||
handler: lookup(&labels, entry.handler_pc)?,
|
||||
catch_type: catch_type(pool, entry.catch_type)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?;
|
||||
|
||||
let mut frames = Vec::new();
|
||||
let mut rest = Vec::new();
|
||||
for attr in attributes {
|
||||
match attr {
|
||||
RtAttr::StackMapTable { frames: found, .. } => {
|
||||
if !frames.is_empty() {
|
||||
return Err(ClassfileError::Parse(
|
||||
"два StackMapTable в одном Code".into(),
|
||||
));
|
||||
}
|
||||
frames = to_frames(found, pool)?;
|
||||
}
|
||||
RtAttr::LineNumberTable { line_numbers, .. } => rest.push(Attribute::LineNumberTable(
|
||||
line_numbers
|
||||
.iter()
|
||||
.map(|line| {
|
||||
Ok(LineNumber {
|
||||
start: lookup(&labels, line.start_pc)?,
|
||||
line: line.line_number,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
)),
|
||||
RtAttr::LocalVariableTable { variables, .. } => {
|
||||
rest.push(Attribute::LocalVariableTable(
|
||||
variables
|
||||
.iter()
|
||||
.map(|var| {
|
||||
Ok(LocalVariable {
|
||||
start: resolve_byte(&labels, &starts, var.start_pc)?,
|
||||
end: resolve_end(
|
||||
&labels,
|
||||
&starts,
|
||||
var.start_pc,
|
||||
var.length,
|
||||
code_length,
|
||||
)?,
|
||||
name: super::pool::utf8(pool, var.name_index)?,
|
||||
descriptor: super::pool::utf8(pool, var.descriptor_index)?,
|
||||
index: var.index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
))
|
||||
}
|
||||
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
|
||||
rest.push(Attribute::LocalVariableTypeTable(
|
||||
variable_types
|
||||
.iter()
|
||||
.map(|var| {
|
||||
Ok(LocalVariableType {
|
||||
start: resolve_byte(&labels, &starts, var.start_pc)?,
|
||||
end: resolve_end(
|
||||
&labels,
|
||||
&starts,
|
||||
var.start_pc,
|
||||
var.length,
|
||||
code_length,
|
||||
)?,
|
||||
name: super::pool::utf8(pool, var.name_index)?,
|
||||
signature: super::pool::utf8(pool, var.signature_index)?,
|
||||
index: var.index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
));
|
||||
}
|
||||
other => rest.push(attrs::convert(other, pool)?),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Code {
|
||||
max_stack,
|
||||
max_locals,
|
||||
insns,
|
||||
exception_table,
|
||||
frames,
|
||||
attributes: rest,
|
||||
})
|
||||
}
|
||||
|
||||
/// Байтовые смещения инструкций и длина тела метода.
|
||||
fn layout(code: &[Instruction]) -> Result<(Vec<u16>, u16), ClassfileError> {
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
let mut offsets = Vec::with_capacity(code.len());
|
||||
for instruction in code {
|
||||
offsets.push(tell(&cursor)?);
|
||||
instruction
|
||||
.to_bytes(&mut cursor)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
}
|
||||
Ok((offsets, tell(&cursor)?))
|
||||
}
|
||||
|
||||
fn tell(cursor: &Cursor<Vec<u8>>) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(cursor.position())
|
||||
.map_err(|_| ClassfileError::Parse("тело метода длиннее 64 КБ".into()))
|
||||
}
|
||||
203
crates/noxis-classfile/src/format/parse/insns/basic.rs
Normal file
203
crates/noxis-classfile/src/format/parse/insns/basic.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
//! Простые инструкции без ссылок на пул (кроме `ldc`) и без ветвлений.
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::Instruction;
|
||||
|
||||
use super::super::pool::const_from;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Arith, ArrayKind, Const, Convert, Insn, Prim, SlotKind};
|
||||
|
||||
/// `Some` — инструкция покрыта, `None` — отдать следующим конвертерам.
|
||||
pub(super) fn convert_basic(
|
||||
instruction: &Instruction,
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Option<Insn>, ClassfileError> {
|
||||
Ok(Some(match instruction {
|
||||
Instruction::Nop => Insn::Nop,
|
||||
Instruction::Aconst_null => Insn::AconstNull,
|
||||
Instruction::Iconst_m1 => Insn::Const(Const::Int(-1)),
|
||||
Instruction::Iconst_0 => Insn::Const(Const::Int(0)),
|
||||
Instruction::Iconst_1 => Insn::Const(Const::Int(1)),
|
||||
Instruction::Iconst_2 => Insn::Const(Const::Int(2)),
|
||||
Instruction::Iconst_3 => Insn::Const(Const::Int(3)),
|
||||
Instruction::Iconst_4 => Insn::Const(Const::Int(4)),
|
||||
Instruction::Iconst_5 => Insn::Const(Const::Int(5)),
|
||||
Instruction::Lconst_0 => Insn::Const(Const::Long(0)),
|
||||
Instruction::Lconst_1 => Insn::Const(Const::Long(1)),
|
||||
Instruction::Fconst_0 => Insn::Const(Const::Float(0.0)),
|
||||
Instruction::Fconst_1 => Insn::Const(Const::Float(1.0)),
|
||||
Instruction::Fconst_2 => Insn::Const(Const::Float(2.0)),
|
||||
Instruction::Dconst_0 => Insn::Const(Const::Double(0.0)),
|
||||
Instruction::Dconst_1 => Insn::Const(Const::Double(1.0)),
|
||||
Instruction::Bipush(v) => Insn::Const(Const::Int(i32::from(*v))),
|
||||
Instruction::Sipush(v) => Insn::Const(Const::Int(i32::from(*v))),
|
||||
Instruction::Ldc(i) => Insn::Const(const_from(pool, u16::from(*i))?),
|
||||
Instruction::Ldc_w(i) | Instruction::Ldc2_w(i) => Insn::Const(const_from(pool, *i)?),
|
||||
Instruction::Iload(i) => load(u16::from(*i), SlotKind::Int),
|
||||
Instruction::Lload(i) => load(u16::from(*i), SlotKind::Long),
|
||||
Instruction::Fload(i) => load(u16::from(*i), SlotKind::Float),
|
||||
Instruction::Dload(i) => load(u16::from(*i), SlotKind::Double),
|
||||
Instruction::Aload(i) => load(u16::from(*i), SlotKind::Ref),
|
||||
Instruction::Iload_w(i) => load(*i, SlotKind::Int),
|
||||
Instruction::Lload_w(i) => load(*i, SlotKind::Long),
|
||||
Instruction::Fload_w(i) => load(*i, SlotKind::Float),
|
||||
Instruction::Dload_w(i) => load(*i, SlotKind::Double),
|
||||
Instruction::Aload_w(i) => load(*i, SlotKind::Ref),
|
||||
Instruction::Iload_0 => load(0, SlotKind::Int),
|
||||
Instruction::Iload_1 => load(1, SlotKind::Int),
|
||||
Instruction::Iload_2 => load(2, SlotKind::Int),
|
||||
Instruction::Iload_3 => load(3, SlotKind::Int),
|
||||
Instruction::Lload_0 => load(0, SlotKind::Long),
|
||||
Instruction::Lload_1 => load(1, SlotKind::Long),
|
||||
Instruction::Lload_2 => load(2, SlotKind::Long),
|
||||
Instruction::Lload_3 => load(3, SlotKind::Long),
|
||||
Instruction::Fload_0 => load(0, SlotKind::Float),
|
||||
Instruction::Fload_1 => load(1, SlotKind::Float),
|
||||
Instruction::Fload_2 => load(2, SlotKind::Float),
|
||||
Instruction::Fload_3 => load(3, SlotKind::Float),
|
||||
Instruction::Dload_0 => load(0, SlotKind::Double),
|
||||
Instruction::Dload_1 => load(1, SlotKind::Double),
|
||||
Instruction::Dload_2 => load(2, SlotKind::Double),
|
||||
Instruction::Dload_3 => load(3, SlotKind::Double),
|
||||
Instruction::Aload_0 => load(0, SlotKind::Ref),
|
||||
Instruction::Aload_1 => load(1, SlotKind::Ref),
|
||||
Instruction::Aload_2 => load(2, SlotKind::Ref),
|
||||
Instruction::Aload_3 => load(3, SlotKind::Ref),
|
||||
Instruction::Istore(i) => store(u16::from(*i), SlotKind::Int),
|
||||
Instruction::Lstore(i) => store(u16::from(*i), SlotKind::Long),
|
||||
Instruction::Fstore(i) => store(u16::from(*i), SlotKind::Float),
|
||||
Instruction::Dstore(i) => store(u16::from(*i), SlotKind::Double),
|
||||
Instruction::Astore(i) => store(u16::from(*i), SlotKind::Ref),
|
||||
Instruction::Istore_w(i) => store(*i, SlotKind::Int),
|
||||
Instruction::Lstore_w(i) => store(*i, SlotKind::Long),
|
||||
Instruction::Fstore_w(i) => store(*i, SlotKind::Float),
|
||||
Instruction::Dstore_w(i) => store(*i, SlotKind::Double),
|
||||
Instruction::Astore_w(i) => store(*i, SlotKind::Ref),
|
||||
Instruction::Istore_0 => store(0, SlotKind::Int),
|
||||
Instruction::Istore_1 => store(1, SlotKind::Int),
|
||||
Instruction::Istore_2 => store(2, SlotKind::Int),
|
||||
Instruction::Istore_3 => store(3, SlotKind::Int),
|
||||
Instruction::Lstore_0 => store(0, SlotKind::Long),
|
||||
Instruction::Lstore_1 => store(1, SlotKind::Long),
|
||||
Instruction::Lstore_2 => store(2, SlotKind::Long),
|
||||
Instruction::Lstore_3 => store(3, SlotKind::Long),
|
||||
Instruction::Fstore_0 => store(0, SlotKind::Float),
|
||||
Instruction::Fstore_1 => store(1, SlotKind::Float),
|
||||
Instruction::Fstore_2 => store(2, SlotKind::Float),
|
||||
Instruction::Fstore_3 => store(3, SlotKind::Float),
|
||||
Instruction::Dstore_0 => store(0, SlotKind::Double),
|
||||
Instruction::Dstore_1 => store(1, SlotKind::Double),
|
||||
Instruction::Dstore_2 => store(2, SlotKind::Double),
|
||||
Instruction::Dstore_3 => store(3, SlotKind::Double),
|
||||
Instruction::Astore_0 => store(0, SlotKind::Ref),
|
||||
Instruction::Astore_1 => store(1, SlotKind::Ref),
|
||||
Instruction::Astore_2 => store(2, SlotKind::Ref),
|
||||
Instruction::Astore_3 => store(3, SlotKind::Ref),
|
||||
Instruction::Iinc(i, d) => Insn::Iinc {
|
||||
index: u16::from(*i),
|
||||
delta: i32::from(*d),
|
||||
},
|
||||
Instruction::Iinc_w(i, d) => Insn::Iinc {
|
||||
index: *i,
|
||||
delta: i32::from(*d),
|
||||
},
|
||||
Instruction::Baload => Insn::ArrayLoad(ArrayKind::Byte),
|
||||
Instruction::Caload => Insn::ArrayLoad(ArrayKind::Char),
|
||||
Instruction::Saload => Insn::ArrayLoad(ArrayKind::Short),
|
||||
Instruction::Iaload => Insn::ArrayLoad(ArrayKind::Int),
|
||||
Instruction::Laload => Insn::ArrayLoad(ArrayKind::Long),
|
||||
Instruction::Faload => Insn::ArrayLoad(ArrayKind::Float),
|
||||
Instruction::Daload => Insn::ArrayLoad(ArrayKind::Double),
|
||||
Instruction::Aaload => Insn::ArrayLoad(ArrayKind::Reference),
|
||||
Instruction::Bastore => Insn::ArrayStore(ArrayKind::Byte),
|
||||
Instruction::Castore => Insn::ArrayStore(ArrayKind::Char),
|
||||
Instruction::Sastore => Insn::ArrayStore(ArrayKind::Short),
|
||||
Instruction::Iastore => Insn::ArrayStore(ArrayKind::Int),
|
||||
Instruction::Lastore => Insn::ArrayStore(ArrayKind::Long),
|
||||
Instruction::Fastore => Insn::ArrayStore(ArrayKind::Float),
|
||||
Instruction::Dastore => Insn::ArrayStore(ArrayKind::Double),
|
||||
Instruction::Aastore => Insn::ArrayStore(ArrayKind::Reference),
|
||||
Instruction::Pop => Insn::Pop,
|
||||
Instruction::Pop2 => Insn::Pop2,
|
||||
Instruction::Dup => Insn::Dup,
|
||||
Instruction::Dup_x1 => Insn::DupX1,
|
||||
Instruction::Dup_x2 => Insn::DupX2,
|
||||
Instruction::Dup2 => Insn::Dup2,
|
||||
Instruction::Dup2_x1 => Insn::Dup2X1,
|
||||
Instruction::Dup2_x2 => Insn::Dup2X2,
|
||||
Instruction::Swap => Insn::Swap,
|
||||
Instruction::Iadd => binary(Prim::Int, Arith::Add),
|
||||
Instruction::Isub => binary(Prim::Int, Arith::Sub),
|
||||
Instruction::Imul => binary(Prim::Int, Arith::Mul),
|
||||
Instruction::Idiv => binary(Prim::Int, Arith::Div),
|
||||
Instruction::Irem => binary(Prim::Int, Arith::Rem),
|
||||
Instruction::Ineg => binary(Prim::Int, Arith::Neg),
|
||||
Instruction::Iand => binary(Prim::Int, Arith::And),
|
||||
Instruction::Ior => binary(Prim::Int, Arith::Or),
|
||||
Instruction::Ixor => binary(Prim::Int, Arith::Xor),
|
||||
Instruction::Ishl => binary(Prim::Int, Arith::Shl),
|
||||
Instruction::Ishr => binary(Prim::Int, Arith::Shr),
|
||||
Instruction::Iushr => binary(Prim::Int, Arith::Ushr),
|
||||
Instruction::Ladd => binary(Prim::Long, Arith::Add),
|
||||
Instruction::Lsub => binary(Prim::Long, Arith::Sub),
|
||||
Instruction::Lmul => binary(Prim::Long, Arith::Mul),
|
||||
Instruction::Ldiv => binary(Prim::Long, Arith::Div),
|
||||
Instruction::Lrem => binary(Prim::Long, Arith::Rem),
|
||||
Instruction::Lneg => binary(Prim::Long, Arith::Neg),
|
||||
Instruction::Land => binary(Prim::Long, Arith::And),
|
||||
Instruction::Lor => binary(Prim::Long, Arith::Or),
|
||||
Instruction::Lxor => binary(Prim::Long, Arith::Xor),
|
||||
Instruction::Lshl => binary(Prim::Long, Arith::Shl),
|
||||
Instruction::Lshr => binary(Prim::Long, Arith::Shr),
|
||||
Instruction::Lushr => binary(Prim::Long, Arith::Ushr),
|
||||
Instruction::Fadd => binary(Prim::Float, Arith::Add),
|
||||
Instruction::Fsub => binary(Prim::Float, Arith::Sub),
|
||||
Instruction::Fmul => binary(Prim::Float, Arith::Mul),
|
||||
Instruction::Fdiv => binary(Prim::Float, Arith::Div),
|
||||
Instruction::Frem => binary(Prim::Float, Arith::Rem),
|
||||
Instruction::Fneg => binary(Prim::Float, Arith::Neg),
|
||||
Instruction::Dadd => binary(Prim::Double, Arith::Add),
|
||||
Instruction::Dsub => binary(Prim::Double, Arith::Sub),
|
||||
Instruction::Dmul => binary(Prim::Double, Arith::Mul),
|
||||
Instruction::Ddiv => binary(Prim::Double, Arith::Div),
|
||||
Instruction::Drem => binary(Prim::Double, Arith::Rem),
|
||||
Instruction::Dneg => binary(Prim::Double, Arith::Neg),
|
||||
Instruction::Lcmp => cmp(Prim::Long, false),
|
||||
Instruction::Fcmpl => cmp(Prim::Float, false),
|
||||
Instruction::Fcmpg => cmp(Prim::Float, true),
|
||||
Instruction::Dcmpl => cmp(Prim::Double, false),
|
||||
Instruction::Dcmpg => cmp(Prim::Double, true),
|
||||
Instruction::I2l => Insn::Convert(Convert::I2L),
|
||||
Instruction::I2f => Insn::Convert(Convert::I2F),
|
||||
Instruction::I2d => Insn::Convert(Convert::I2D),
|
||||
Instruction::I2b => Insn::Convert(Convert::I2B),
|
||||
Instruction::I2c => Insn::Convert(Convert::I2C),
|
||||
Instruction::I2s => Insn::Convert(Convert::I2S),
|
||||
Instruction::L2i => Insn::Convert(Convert::L2I),
|
||||
Instruction::L2f => Insn::Convert(Convert::L2F),
|
||||
Instruction::L2d => Insn::Convert(Convert::L2D),
|
||||
Instruction::F2i => Insn::Convert(Convert::F2I),
|
||||
Instruction::F2l => Insn::Convert(Convert::F2L),
|
||||
Instruction::F2d => Insn::Convert(Convert::F2D),
|
||||
Instruction::D2i => Insn::Convert(Convert::D2I),
|
||||
Instruction::D2l => Insn::Convert(Convert::D2L),
|
||||
Instruction::D2f => Insn::Convert(Convert::D2F),
|
||||
_ => return Ok(None),
|
||||
}))
|
||||
}
|
||||
|
||||
fn load(slot: u16, kind: SlotKind) -> Insn {
|
||||
Insn::Load { slot, kind }
|
||||
}
|
||||
|
||||
fn store(slot: u16, kind: SlotKind) -> Insn {
|
||||
Insn::Store { slot, kind }
|
||||
}
|
||||
|
||||
fn binary(prim: Prim, op: Arith) -> Insn {
|
||||
Insn::Binary { prim, op }
|
||||
}
|
||||
|
||||
fn cmp(prim: Prim, nan_great: bool) -> Insn {
|
||||
Insn::Cmp { prim, nan_great }
|
||||
}
|
||||
98
crates/noxis-classfile/src/format/parse/insns/flow.rs
Normal file
98
crates/noxis-classfile/src/format/parse/insns/flow.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
//! Ветвления, `switch` и возвраты: абсолютные/относительные цели → метки.
|
||||
|
||||
use ristretto_classfile::attributes::{Instruction, LookupSwitch, TableSwitch};
|
||||
|
||||
use super::Resolver;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
|
||||
|
||||
/// `Some` — инструкция покрыта, `None` — отдать следующим конвертерам.
|
||||
pub(super) fn convert_flow(
|
||||
instruction: &Instruction,
|
||||
index: u16,
|
||||
resolve: &Resolver<'_>,
|
||||
) -> Result<Option<Insn>, ClassfileError> {
|
||||
let rel = |delta: i32| -> Result<Label, ClassfileError> { abs(index, delta, resolve) };
|
||||
Ok(Some(match instruction {
|
||||
Instruction::Ifeq(t) => branch(BranchCond::Zero(Cond::Eq), *t, resolve)?,
|
||||
Instruction::Ifne(t) => branch(BranchCond::Zero(Cond::Ne), *t, resolve)?,
|
||||
Instruction::Iflt(t) => branch(BranchCond::Zero(Cond::Lt), *t, resolve)?,
|
||||
Instruction::Ifge(t) => branch(BranchCond::Zero(Cond::Ge), *t, resolve)?,
|
||||
Instruction::Ifgt(t) => branch(BranchCond::Zero(Cond::Gt), *t, resolve)?,
|
||||
Instruction::Ifle(t) => branch(BranchCond::Zero(Cond::Le), *t, resolve)?,
|
||||
Instruction::If_icmpeq(t) => branch(BranchCond::Int(Cond::Eq), *t, resolve)?,
|
||||
Instruction::If_icmpne(t) => branch(BranchCond::Int(Cond::Ne), *t, resolve)?,
|
||||
Instruction::If_icmplt(t) => branch(BranchCond::Int(Cond::Lt), *t, resolve)?,
|
||||
Instruction::If_icmpge(t) => branch(BranchCond::Int(Cond::Ge), *t, resolve)?,
|
||||
Instruction::If_icmpgt(t) => branch(BranchCond::Int(Cond::Gt), *t, resolve)?,
|
||||
Instruction::If_icmple(t) => branch(BranchCond::Int(Cond::Le), *t, resolve)?,
|
||||
Instruction::If_acmpeq(t) => branch(BranchCond::Ref { eq: true }, *t, resolve)?,
|
||||
Instruction::If_acmpne(t) => branch(BranchCond::Ref { eq: false }, *t, resolve)?,
|
||||
Instruction::Ifnull(t) => branch(BranchCond::Null { is_null: true }, *t, resolve)?,
|
||||
Instruction::Ifnonnull(t) => branch(BranchCond::Null { is_null: false }, *t, resolve)?,
|
||||
Instruction::Goto(t) => Insn::Goto(resolve(*t)?),
|
||||
Instruction::Goto_w(t) => Insn::Goto(resolve(wide(*t)?)?),
|
||||
Instruction::Jsr(t) => Insn::Jsr(resolve(*t)?),
|
||||
Instruction::Jsr_w(t) => Insn::Jsr(resolve(wide(*t)?)?),
|
||||
Instruction::Ret(i) => Insn::Ret(u16::from(*i)),
|
||||
Instruction::Ret_w(i) => Insn::Ret(*i),
|
||||
Instruction::Tableswitch(s) => switch_table(s, &rel)?,
|
||||
Instruction::Lookupswitch(s) => switch_lookup(s, &rel)?,
|
||||
Instruction::Return => Insn::Return(None),
|
||||
Instruction::Ireturn => Insn::Return(Some(Prim::Int)),
|
||||
Instruction::Lreturn => Insn::Return(Some(Prim::Long)),
|
||||
Instruction::Freturn => Insn::Return(Some(Prim::Float)),
|
||||
Instruction::Dreturn => Insn::Return(Some(Prim::Double)),
|
||||
Instruction::Areturn => Insn::AReturn,
|
||||
_ => return Ok(None),
|
||||
}))
|
||||
}
|
||||
|
||||
fn branch(cond: BranchCond, target: u16, resolve: &Resolver<'_>) -> Result<Insn, ClassfileError> {
|
||||
Ok(Insn::Branch(cond, resolve(target)?))
|
||||
}
|
||||
|
||||
/// Цель `switch` — номер инструкции: база плюс относительная дельта backend'а.
|
||||
fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result<Label, ClassfileError> {
|
||||
let absolute = i32::from(index)
|
||||
.checked_add(delta)
|
||||
.ok_or_else(|| ClassfileError::Parse("переполнение смещения ветвления".into()))?;
|
||||
let index = u16::try_from(absolute)
|
||||
.map_err(|_| ClassfileError::Parse("смещение ветвления вне диапазона u16".into()))?;
|
||||
resolve(index)
|
||||
}
|
||||
|
||||
fn wide(target: i32) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(target)
|
||||
.map_err(|_| ClassfileError::Parse("широкий переход вне диапазона u16".into()))
|
||||
}
|
||||
|
||||
fn switch_table(
|
||||
table: &TableSwitch,
|
||||
rel: &dyn Fn(i32) -> Result<Label, ClassfileError>,
|
||||
) -> Result<Insn, ClassfileError> {
|
||||
Ok(Insn::TableSwitch {
|
||||
default: rel(table.default)?,
|
||||
low: table.low,
|
||||
high: table.high,
|
||||
targets: table
|
||||
.offsets
|
||||
.iter()
|
||||
.map(|offset| rel(*offset))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn switch_lookup(
|
||||
lookup: &LookupSwitch,
|
||||
rel: &dyn Fn(i32) -> Result<Label, ClassfileError>,
|
||||
) -> Result<Insn, ClassfileError> {
|
||||
Ok(Insn::LookupSwitch {
|
||||
default: rel(lookup.default)?,
|
||||
pairs: lookup
|
||||
.pairs
|
||||
.iter()
|
||||
.map(|(key, offset)| Ok((*key, rel(*offset)?)))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
48
crates/noxis-classfile/src/format/parse/insns/mod.rs
Normal file
48
crates/noxis-classfile/src/format/parse/insns/mod.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! `Instruction` backend'а → [`Insn`](crate::insn::Insn) нашего IR.
|
||||
//!
|
||||
//! Смещения ветвлений приходят из backend'а (абсолютные для `if*`/`goto`,
|
||||
//! относительные от начала инструкции для `switch`), поэтому конвертация
|
||||
//! принимает резолвер смещения в [`Label`](crate::insn::Label) — метки
|
||||
//! назначает вызывающий код.
|
||||
|
||||
mod basic;
|
||||
mod flow;
|
||||
mod refs;
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::Instruction;
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Insn, Label};
|
||||
|
||||
/// Резолвит абсолютное смещение байткода в метку.
|
||||
pub(super) type Resolver<'a> = dyn Fn(u16) -> Result<Label, ClassfileError> + 'a;
|
||||
|
||||
/// Конвертировать одну инструкцию. `index` — её номер в теле метода (нужно `switch`).
|
||||
pub(super) fn convert(
|
||||
instruction: &Instruction,
|
||||
index: u16,
|
||||
pool: &ConstantPool<'_>,
|
||||
resolve: &Resolver<'_>,
|
||||
) -> Result<Insn, ClassfileError> {
|
||||
if let Some(insn) = basic::convert_basic(instruction, pool)? {
|
||||
return Ok(insn);
|
||||
}
|
||||
if let Some(insn) = flow::convert_flow(instruction, index, resolve)? {
|
||||
return Ok(insn);
|
||||
}
|
||||
if let Some(insn) = refs::convert_ref(instruction, pool)? {
|
||||
return Ok(insn);
|
||||
}
|
||||
match instruction {
|
||||
Instruction::Breakpoint | Instruction::Impdep1 | Instruction::Impdep2 => Err(
|
||||
ClassfileError::Parse("зарезервированный опкод в коде метода".into()),
|
||||
),
|
||||
Instruction::Wide => Err(ClassfileError::Parse(
|
||||
"wide обязан быть развёрнут парсером backend'а".into(),
|
||||
)),
|
||||
other => Err(ClassfileError::Parse(format!(
|
||||
"неизвестная инструкция backend'а: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
102
crates/noxis-classfile/src/format/parse/insns/refs.rs
Normal file
102
crates/noxis-classfile/src/format/parse/insns/refs.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//! Поля, вызовы и создание объектов: индексы пула → строки IR.
|
||||
|
||||
use ristretto_classfile::ConstantPool;
|
||||
use ristretto_classfile::attributes::{ArrayType, Instruction};
|
||||
|
||||
use super::super::pool::{class_name, dynamic_parts, expect_kind, is_interface, ref_parts};
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{ArrayKind, Insn, InvokeKind};
|
||||
|
||||
/// `Some` — инструкция покрыта, `None` — отдать следующим конвертерам.
|
||||
pub(super) fn convert_ref(
|
||||
instruction: &Instruction,
|
||||
pool: &ConstantPool<'_>,
|
||||
) -> Result<Option<Insn>, ClassfileError> {
|
||||
Ok(Some(match instruction {
|
||||
Instruction::Getstatic(i) => {
|
||||
let (owner, name, descriptor) = ref_parts(pool, *i)?;
|
||||
Insn::GetStatic {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
}
|
||||
Instruction::Putstatic(i) => {
|
||||
let (owner, name, descriptor) = ref_parts(pool, *i)?;
|
||||
Insn::PutStatic {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
}
|
||||
Instruction::Getfield(i) => {
|
||||
let (owner, name, descriptor) = ref_parts(pool, *i)?;
|
||||
Insn::GetField {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
}
|
||||
Instruction::Putfield(i) => {
|
||||
let (owner, name, descriptor) = ref_parts(pool, *i)?;
|
||||
Insn::PutField {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
}
|
||||
Instruction::Invokevirtual(i) => invoke(pool, *i, InvokeKind::Virtual)?,
|
||||
Instruction::Invokespecial(i) => invoke(pool, *i, InvokeKind::Special)?,
|
||||
Instruction::Invokestatic(i) => invoke(pool, *i, InvokeKind::Static)?,
|
||||
Instruction::Invokeinterface(i, _) => invoke(pool, *i, InvokeKind::Interface)?,
|
||||
Instruction::Invokedynamic(i) => dynamic(pool, *i)?,
|
||||
Instruction::New(i) => Insn::New(class_name(pool, *i)?),
|
||||
Instruction::Newarray(t) => Insn::NewArray(array_kind(t.clone())),
|
||||
Instruction::Anewarray(i) => Insn::ANewArray(class_name(pool, *i)?),
|
||||
Instruction::Multianewarray(i, dims) => Insn::MultiANewArray {
|
||||
descriptor: class_name(pool, *i)?,
|
||||
dims: *dims,
|
||||
},
|
||||
Instruction::Arraylength => Insn::ArrayLength,
|
||||
Instruction::Athrow => Insn::AThrow,
|
||||
Instruction::Checkcast(i) => Insn::CheckCast(class_name(pool, *i)?),
|
||||
Instruction::Instanceof(i) => Insn::InstanceOf(class_name(pool, *i)?),
|
||||
Instruction::Monitorenter => Insn::MonitorEnter,
|
||||
Instruction::Monitorexit => Insn::MonitorExit,
|
||||
_ => return Ok(None),
|
||||
}))
|
||||
}
|
||||
|
||||
fn invoke(pool: &ConstantPool<'_>, index: u16, kind: InvokeKind) -> Result<Insn, ClassfileError> {
|
||||
expect_kind(pool, index, kind)?;
|
||||
let (owner, name, descriptor) = ref_parts(pool, index)?;
|
||||
Ok(Insn::Invoke {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
itf: is_interface(pool, index),
|
||||
})
|
||||
}
|
||||
|
||||
fn dynamic(pool: &ConstantPool<'_>, index: u16) -> Result<Insn, ClassfileError> {
|
||||
let (name, descriptor, bootstrap) = dynamic_parts(pool, index)?;
|
||||
Ok(Insn::InvokeDynamic {
|
||||
name,
|
||||
descriptor,
|
||||
bootstrap,
|
||||
})
|
||||
}
|
||||
|
||||
fn array_kind(kind: ArrayType) -> ArrayKind {
|
||||
match kind {
|
||||
ArrayType::Boolean => ArrayKind::Boolean,
|
||||
ArrayType::Char => ArrayKind::Char,
|
||||
ArrayType::Float => ArrayKind::Float,
|
||||
ArrayType::Double => ArrayKind::Double,
|
||||
ArrayType::Byte => ArrayKind::Byte,
|
||||
ArrayType::Short => ArrayKind::Short,
|
||||
ArrayType::Int => ArrayKind::Int,
|
||||
ArrayType::Long => ArrayKind::Long,
|
||||
}
|
||||
}
|
||||
12
crates/noxis-classfile/src/format/parse/mod.rs
Normal file
12
crates/noxis-classfile/src/format/parse/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//! Backend `ristretto_classfile` → наш IR: пул, инструкции, тела методов.
|
||||
//!
|
||||
//! Пока ниоткуда не подключён (P1): `format` вернёт `Unimplemented`, пока не
|
||||
//! будут готовы `parse`/`write` классового уровня поверх этих конвертеров.
|
||||
//! Дерево уже разбито по ответственностям, чтобы подключение было одним
|
||||
//! `mod parse` без рефакторинга.
|
||||
|
||||
pub(super) mod attrs;
|
||||
pub(super) mod class;
|
||||
pub(super) mod code;
|
||||
pub(super) mod insns;
|
||||
pub(super) mod pool;
|
||||
184
crates/noxis-classfile/src/format/parse/pool.rs
Normal file
184
crates/noxis-classfile/src/format/parse/pool.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
//! Разрешение индексов constant pool в строки нашего IR.
|
||||
|
||||
use ristretto_classfile::{Constant, ConstantPool, ReferenceKind};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Const, InvokeKind};
|
||||
|
||||
/// Имя класса по `CONSTANT_Class`.
|
||||
pub(super) fn class_name(pool: &ConstantPool<'_>, index: u16) -> Result<String, ClassfileError> {
|
||||
let name = pool
|
||||
.try_get_class(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
Ok(name.to_string())
|
||||
}
|
||||
|
||||
/// `(owner, name, descriptor)` для `CONSTANT_Fieldref`/`_Methodref`.
|
||||
pub(super) fn ref_parts(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
) -> Result<(String, String, String), ClassfileError> {
|
||||
let constant = pool
|
||||
.try_get(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let (class_index, name_and_type_index) = match constant {
|
||||
Constant::FieldRef {
|
||||
class_index,
|
||||
name_and_type_index,
|
||||
}
|
||||
| Constant::MethodRef {
|
||||
class_index,
|
||||
name_and_type_index,
|
||||
}
|
||||
| Constant::InterfaceMethodRef {
|
||||
class_index,
|
||||
name_and_type_index,
|
||||
} => (*class_index, *name_and_type_index),
|
||||
other => {
|
||||
return Err(ClassfileError::Parse(format!(
|
||||
"ожидалась ссылка на поле/метод, найден {other:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let owner = class_name(pool, class_index)?;
|
||||
let (name, descriptor) = name_and_type(pool, name_and_type_index)?;
|
||||
Ok((owner, name, descriptor))
|
||||
}
|
||||
|
||||
/// `(name, descriptor)` для `CONSTANT_NameAndType`.
|
||||
pub(super) fn name_and_type(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
) -> Result<(String, String), ClassfileError> {
|
||||
let (name_index, descriptor_index) = pool
|
||||
.try_get_name_and_type(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let name = utf8(pool, *name_index)?;
|
||||
let descriptor = utf8(pool, *descriptor_index)?;
|
||||
Ok((name, descriptor))
|
||||
}
|
||||
|
||||
/// Строка из `CONSTANT_Utf8`.
|
||||
pub(super) fn utf8(pool: &ConstantPool<'_>, index: u16) -> Result<String, ClassfileError> {
|
||||
let value = pool
|
||||
.try_get_utf8(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
/// Константа для `ldc`/`ldc_w`/`ldc2_w`.
|
||||
pub(super) fn const_from(pool: &ConstantPool<'_>, index: u16) -> Result<Const, ClassfileError> {
|
||||
let constant = pool
|
||||
.try_get(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
Ok(match constant {
|
||||
Constant::Integer(v) => Const::Int(*v),
|
||||
Constant::Float(v) => Const::Float(*v),
|
||||
Constant::Long(v) => Const::Long(*v),
|
||||
Constant::Double(v) => Const::Double(*v),
|
||||
Constant::String(v) => Const::String(utf8(pool, *v)?),
|
||||
Constant::Class(v) => Const::Class(utf8(pool, *v)?),
|
||||
Constant::MethodType(v) => Const::MethodType(utf8(pool, *v)?),
|
||||
Constant::MethodHandle {
|
||||
reference_kind,
|
||||
reference_index,
|
||||
} => Const::MethodHandle {
|
||||
kind: kind_code(reference_kind),
|
||||
owner: ref_parts(pool, *reference_index)?.0,
|
||||
name: ref_parts(pool, *reference_index)?.1,
|
||||
descriptor: ref_parts(pool, *reference_index)?.2,
|
||||
itf: matches!(
|
||||
pool.try_get(*reference_index),
|
||||
Ok(Constant::InterfaceMethodRef { .. })
|
||||
),
|
||||
},
|
||||
Constant::Dynamic {
|
||||
bootstrap_method_attr_index,
|
||||
name_and_type_index,
|
||||
} => {
|
||||
let (name, descriptor) = name_and_type(pool, *name_and_type_index)?;
|
||||
Const::Dynamic {
|
||||
name,
|
||||
descriptor,
|
||||
bootstrap: *bootstrap_method_attr_index,
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(ClassfileError::Parse(format!(
|
||||
"константа {other:?} недопустима в ldc"
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `(name, descriptor, bootstrap)` для `CONSTANT_InvokeDynamic`.
|
||||
pub(super) fn dynamic_parts(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
) -> Result<(String, String, u16), ClassfileError> {
|
||||
let constant = pool
|
||||
.try_get(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let (bootstrap, name_and_type_index) = match constant {
|
||||
Constant::InvokeDynamic {
|
||||
bootstrap_method_attr_index,
|
||||
name_and_type_index,
|
||||
} => (*bootstrap_method_attr_index, *name_and_type_index),
|
||||
other => {
|
||||
return Err(ClassfileError::Parse(format!(
|
||||
"ожидалась InvokeDynamic, найден {other:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let (name, descriptor) = name_and_type(pool, name_and_type_index)?;
|
||||
Ok((name, descriptor, bootstrap))
|
||||
}
|
||||
|
||||
/// Вид вызова по индексу ссылки: интерфейсная или обычная.
|
||||
pub(super) fn is_interface(pool: &ConstantPool<'_>, index: u16) -> bool {
|
||||
matches!(pool.try_get(index), Ok(Constant::InterfaceMethodRef { .. }))
|
||||
}
|
||||
|
||||
/// JVMS §5.4.3.5: номер `reference_kind` для `CONSTANT_MethodHandle`.
|
||||
pub(super) fn kind_code(kind: &ReferenceKind) -> u8 {
|
||||
match kind {
|
||||
ReferenceKind::GetField => 1,
|
||||
ReferenceKind::GetStatic => 2,
|
||||
ReferenceKind::PutField => 3,
|
||||
ReferenceKind::PutStatic => 4,
|
||||
ReferenceKind::InvokeVirtual => 5,
|
||||
ReferenceKind::InvokeStatic => 6,
|
||||
ReferenceKind::InvokeSpecial => 7,
|
||||
ReferenceKind::NewInvokeSpecial => 8,
|
||||
ReferenceKind::InvokeInterface => 9,
|
||||
}
|
||||
}
|
||||
|
||||
/// Проверка, что `Invoke` действительно ссылается на ожидаемый вид.
|
||||
///
|
||||
/// JVM принимает `Methodref` и `InterfaceMethodref` для всех видов вызова,
|
||||
/// кроме `invokeinterface` (там только интерфейсная); строгость сверх JVM
|
||||
/// ломает чтение реальных классов (статические методы интерфейсов).
|
||||
pub(super) fn expect_kind(
|
||||
pool: &ConstantPool<'_>,
|
||||
index: u16,
|
||||
kind: InvokeKind,
|
||||
) -> Result<(), ClassfileError> {
|
||||
let constant = pool
|
||||
.try_get(index)
|
||||
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
|
||||
let expected = match kind {
|
||||
InvokeKind::Interface => matches!(constant, Constant::InterfaceMethodRef { .. }),
|
||||
_ => matches!(
|
||||
constant,
|
||||
Constant::MethodRef { .. } | Constant::InterfaceMethodRef { .. }
|
||||
),
|
||||
};
|
||||
if expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ClassfileError::Parse(format!(
|
||||
"вызов {kind:?} указывает на несовместимую ссылку"
|
||||
)))
|
||||
}
|
||||
}
|
||||
250
crates/noxis-classfile/src/format/write/attrs.rs
Normal file
250
crates/noxis-classfile/src/format/write/attrs.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
//! Атрибуты нашего IR → атрибуты backend'а.
|
||||
//!
|
||||
//! Имена атрибутов (`SourceFile`, …) интернируются в пул здесь же — backend
|
||||
//! хранит только `name_index`, строка восстанавливается при чтении.
|
||||
|
||||
use ristretto_classfile::MethodAccessFlags;
|
||||
use ristretto_classfile::attributes::{
|
||||
Attribute as RtAttr, BootstrapMethod as RtBsm, InnerClass as RtInner, LineNumber as RtLine,
|
||||
LocalVariableTable as RtLocal, LocalVariableTypeTable as RtLocalType,
|
||||
MethodParameter as RtParam, NestedClassAccessFlags, Record as RtRecord,
|
||||
};
|
||||
|
||||
use super::pool::Pool;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Const, Label};
|
||||
use crate::model::{
|
||||
Attribute, BootstrapMethod, InnerClass, LineNumber, LocalVariable, LocalVariableType,
|
||||
MethodParameter,
|
||||
};
|
||||
|
||||
/// Позиции таблиц кода: метки → байты текущего layout.
|
||||
pub(super) struct Positions<'a> {
|
||||
pub resolve: &'a dyn Fn(Label) -> Result<u32, ClassfileError>,
|
||||
pub byte_of: &'a dyn Fn(u32) -> Result<u16, ClassfileError>,
|
||||
}
|
||||
|
||||
/// `LineNumberTable`: backend хранит номера инструкций и маппит сам.
|
||||
pub(super) fn line_number_table(
|
||||
lines: &[LineNumber],
|
||||
pool: &mut Pool,
|
||||
positions: &Positions<'_>,
|
||||
) -> Result<RtAttr, ClassfileError> {
|
||||
Ok(RtAttr::LineNumberTable {
|
||||
name_index: pool.utf8("LineNumberTable")?,
|
||||
line_numbers: lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
let index = (positions.resolve)(line.start)?;
|
||||
Ok(RtLine {
|
||||
start_pc: u16::try_from(index).map_err(|_| {
|
||||
ClassfileError::Write(format!("номер инструкции {index} вне u16"))
|
||||
})?,
|
||||
line_number: line.line,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// `LocalVariableTable`/`TypeTable` с пересчётом диапазона.
|
||||
pub(super) fn local_table(
|
||||
vars: &[LocalVariable],
|
||||
pool: &mut Pool,
|
||||
positions: &Positions<'_>,
|
||||
) -> Result<RtAttr, ClassfileError> {
|
||||
Ok(RtAttr::LocalVariableTable {
|
||||
name_index: pool.utf8("LocalVariableTable")?,
|
||||
variables: vars
|
||||
.iter()
|
||||
.map(|var| {
|
||||
let (start_pc, length) = range(var.start, var.end, positions)?;
|
||||
Ok(RtLocal {
|
||||
start_pc,
|
||||
length,
|
||||
name_index: pool.utf8(&var.name)?,
|
||||
descriptor_index: pool.utf8(&var.descriptor)?,
|
||||
index: var.index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
})
|
||||
}
|
||||
/// Та же таблица для типов (`signature` вместо дескриптора).
|
||||
pub(super) fn local_type_table(
|
||||
vars: &[LocalVariableType],
|
||||
pool: &mut Pool,
|
||||
positions: &Positions<'_>,
|
||||
) -> Result<RtAttr, ClassfileError> {
|
||||
Ok(RtAttr::LocalVariableTypeTable {
|
||||
name_index: pool.utf8("LocalVariableTypeTable")?,
|
||||
variable_types: vars
|
||||
.iter()
|
||||
.map(|var| {
|
||||
let (start_pc, length) = range(var.start, var.end, positions)?;
|
||||
Ok(RtLocalType {
|
||||
start_pc,
|
||||
length,
|
||||
name_index: pool.utf8(&var.name)?,
|
||||
signature_index: pool.utf8(&var.signature)?,
|
||||
index: var.index,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn range(
|
||||
start: Label,
|
||||
end: Label,
|
||||
positions: &Positions<'_>,
|
||||
) -> Result<(u16, u16), ClassfileError> {
|
||||
let first = (positions.byte_of)((positions.resolve)(start)?)?;
|
||||
let last = (positions.byte_of)((positions.resolve)(end)?)?;
|
||||
let length = last
|
||||
.checked_sub(first)
|
||||
.ok_or_else(|| ClassfileError::Write("конец раньше начала".into()))?;
|
||||
Ok((first, length))
|
||||
}
|
||||
|
||||
/// Один наш атрибут → атрибут backend'а.
|
||||
pub(super) fn convert(attr: &Attribute, pool: &mut Pool) -> Result<RtAttr, ClassfileError> {
|
||||
Ok(match attr {
|
||||
Attribute::SourceFile(name) => RtAttr::SourceFile {
|
||||
name_index: pool.utf8("SourceFile")?,
|
||||
source_file_index: pool.utf8(name)?,
|
||||
},
|
||||
Attribute::Signature(value) => RtAttr::Signature {
|
||||
name_index: pool.utf8("Signature")?,
|
||||
signature_index: pool.utf8(value)?,
|
||||
},
|
||||
Attribute::Exceptions(names) => RtAttr::Exceptions {
|
||||
name_index: pool.utf8("Exceptions")?,
|
||||
exception_indexes: names
|
||||
.iter()
|
||||
.map(|name| pool.class(name))
|
||||
.collect::<Result<_, _>>()?,
|
||||
},
|
||||
Attribute::Deprecated => RtAttr::Deprecated {
|
||||
name_index: pool.utf8("Deprecated")?,
|
||||
},
|
||||
Attribute::Synthetic => RtAttr::Synthetic {
|
||||
name_index: pool.utf8("Synthetic")?,
|
||||
},
|
||||
Attribute::InnerClasses(classes) => RtAttr::InnerClasses {
|
||||
name_index: pool.utf8("InnerClasses")?,
|
||||
classes: classes
|
||||
.iter()
|
||||
.map(|class| inner(class, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
},
|
||||
Attribute::ConstantValue(value) => RtAttr::ConstantValue {
|
||||
name_index: pool.utf8("ConstantValue")?,
|
||||
constant_value_index: pool.constant(value)?,
|
||||
},
|
||||
Attribute::BootstrapMethods(methods) => RtAttr::BootstrapMethods {
|
||||
name_index: pool.utf8("BootstrapMethods")?,
|
||||
methods: methods
|
||||
.iter()
|
||||
.map(|method| bootstrap(method, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
},
|
||||
Attribute::EnclosingMethod { class, method } => RtAttr::EnclosingMethod {
|
||||
name_index: pool.utf8("EnclosingMethod")?,
|
||||
class_index: pool.class(class)?,
|
||||
method_index: match method {
|
||||
None => 0,
|
||||
Some((name, descriptor)) => pool.name_and_type(name, descriptor)?,
|
||||
},
|
||||
},
|
||||
Attribute::MethodParameters(params) => RtAttr::MethodParameters {
|
||||
name_index: pool.utf8("MethodParameters")?,
|
||||
parameters: params
|
||||
.iter()
|
||||
.map(|param| method_param(param, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
},
|
||||
Attribute::Raw { name, data } => RtAttr::Unknown {
|
||||
name_index: pool.utf8(name)?,
|
||||
info: data.clone(),
|
||||
},
|
||||
Attribute::LineNumberTable(_)
|
||||
| Attribute::LocalVariableTable(_)
|
||||
| Attribute::LocalVariableTypeTable(_) => {
|
||||
return Err(ClassfileError::Write(
|
||||
"таблицы кода пишет вызывающий код метода".into(),
|
||||
));
|
||||
}
|
||||
Attribute::Record(records) => RtAttr::Record {
|
||||
name_index: pool.utf8("Record")?,
|
||||
records: records
|
||||
.iter()
|
||||
.map(|record| {
|
||||
Ok(RtRecord {
|
||||
name_index: pool.utf8(&record.name)?,
|
||||
descriptor_index: pool.utf8(&record.descriptor)?,
|
||||
attributes: record
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attr| convert(attr, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, ClassfileError>>()?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn inner(class: &InnerClass, pool: &mut Pool) -> Result<RtInner, ClassfileError> {
|
||||
Ok(RtInner {
|
||||
class_info_index: pool.class(&class.inner)?,
|
||||
outer_class_info_index: match &class.outer {
|
||||
None => 0,
|
||||
Some(name) => pool.class(name)?,
|
||||
},
|
||||
name_index: match &class.inner_name {
|
||||
None => 0,
|
||||
Some(name) => pool.utf8(name)?,
|
||||
},
|
||||
access_flags: NestedClassAccessFlags::from_bits_truncate(class.access_flags),
|
||||
})
|
||||
}
|
||||
|
||||
fn bootstrap(method: &BootstrapMethod, pool: &mut Pool) -> Result<RtBsm, ClassfileError> {
|
||||
Ok(RtBsm {
|
||||
bootstrap_method_ref: pool.method_handle(
|
||||
method.kind,
|
||||
&method.owner,
|
||||
&method.name,
|
||||
&method.descriptor,
|
||||
method.itf,
|
||||
)?,
|
||||
arguments: method
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| bootstrap_arg(arg, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Аргумент bootstrap-метода: те же константы, что в `ldc`, плюс `Dynamic`.
|
||||
fn bootstrap_arg(arg: &Const, pool: &mut Pool) -> Result<u16, ClassfileError> {
|
||||
match arg {
|
||||
Const::Dynamic {
|
||||
name,
|
||||
descriptor,
|
||||
bootstrap,
|
||||
} => pool.dynamic(*bootstrap, name, descriptor),
|
||||
_ => pool.constant(arg),
|
||||
}
|
||||
}
|
||||
|
||||
fn method_param(param: &MethodParameter, pool: &mut Pool) -> Result<RtParam, ClassfileError> {
|
||||
Ok(RtParam {
|
||||
name_index: match ¶m.name {
|
||||
None => 0,
|
||||
Some(name) => pool.utf8(name)?,
|
||||
},
|
||||
access_flags: MethodAccessFlags::from_bits_truncate(param.access_flags),
|
||||
})
|
||||
}
|
||||
111
crates/noxis-classfile/src/format/write/code/frames.rs
Normal file
111
crates/noxis-classfile/src/format/write/code/frames.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Кадры `StackMapTable` при записи: вариант в вариант, дельты как есть.
|
||||
//!
|
||||
//! Наш IR хранит кадры в номерах инструкций (как backend после чтения) —
|
||||
//! backend сам пересчитает их в байты при `to_bytes`.
|
||||
|
||||
use ristretto_classfile::attributes::{StackFrame as RtFrame, VerificationType as RtType};
|
||||
|
||||
use super::super::pool::Pool;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::format::{StackMapFrame, VerificationType};
|
||||
|
||||
/// Кадры IR → кадры backend'а (индексные дельты — как есть).
|
||||
pub(super) fn convert_frames(
|
||||
frames: &[StackMapFrame],
|
||||
pool: &mut Pool,
|
||||
) -> Result<Vec<RtFrame>, ClassfileError> {
|
||||
frames
|
||||
.iter()
|
||||
.map(|frame| build_frame(frame, pool))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_frame(frame: &StackMapFrame, pool: &mut Pool) -> Result<RtFrame, ClassfileError> {
|
||||
let delta = frame_delta(frame);
|
||||
let offset = u16::try_from(delta)
|
||||
.map_err(|_| ClassfileError::Write(format!("дельта кадра {delta} вне u16")))?;
|
||||
Ok(match frame {
|
||||
StackMapFrame::Same { .. } => {
|
||||
if delta <= 63 {
|
||||
RtFrame::SameFrame {
|
||||
frame_type: delta as u8,
|
||||
}
|
||||
} else {
|
||||
RtFrame::SameFrameExtended {
|
||||
frame_type: 251,
|
||||
offset_delta: offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
StackMapFrame::SameLocals { local, .. } => {
|
||||
let stack = vec![vtype(local, pool)?];
|
||||
if delta <= 63 {
|
||||
RtFrame::SameLocals1StackItemFrame {
|
||||
frame_type: 64 + delta as u8,
|
||||
stack,
|
||||
}
|
||||
} else {
|
||||
RtFrame::SameLocals1StackItemFrameExtended {
|
||||
frame_type: 247,
|
||||
offset_delta: offset,
|
||||
stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
StackMapFrame::Chop { count, .. } => RtFrame::ChopFrame {
|
||||
frame_type: 251u8.wrapping_sub(*count),
|
||||
offset_delta: offset,
|
||||
},
|
||||
StackMapFrame::Append { locals, .. } => {
|
||||
let count = u8::try_from(locals.len()).map_err(|_| {
|
||||
ClassfileError::Write(format!("append-кадр на {} local'ов", locals.len()))
|
||||
})?;
|
||||
if !(1..=3).contains(&count) {
|
||||
return Err(ClassfileError::Write(format!(
|
||||
"append-кадр на {count} local'ов (нужно 1–3)"
|
||||
)));
|
||||
}
|
||||
RtFrame::AppendFrame {
|
||||
frame_type: 251 + count,
|
||||
offset_delta: offset,
|
||||
locals: types(locals, pool)?,
|
||||
}
|
||||
}
|
||||
StackMapFrame::Full { locals, stack, .. } => RtFrame::FullFrame {
|
||||
frame_type: 255,
|
||||
offset_delta: offset,
|
||||
locals: types(locals, pool)?,
|
||||
stack: types(stack, pool)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn frame_delta(frame: &StackMapFrame) -> u32 {
|
||||
match *frame {
|
||||
StackMapFrame::Same { offset_delta } => offset_delta,
|
||||
StackMapFrame::SameLocals { offset_delta, .. } => offset_delta,
|
||||
StackMapFrame::Chop { offset_delta, .. } => offset_delta,
|
||||
StackMapFrame::Append { offset_delta, .. } => offset_delta,
|
||||
StackMapFrame::Full { offset_delta, .. } => offset_delta,
|
||||
}
|
||||
}
|
||||
|
||||
fn types(values: &[VerificationType], pool: &mut Pool) -> Result<Vec<RtType>, ClassfileError> {
|
||||
values.iter().map(|value| vtype(value, pool)).collect()
|
||||
}
|
||||
|
||||
fn vtype(value: &VerificationType, pool: &mut Pool) -> Result<RtType, ClassfileError> {
|
||||
Ok(match value {
|
||||
VerificationType::Top => RtType::Top,
|
||||
VerificationType::Integer => RtType::Integer,
|
||||
VerificationType::Float => RtType::Float,
|
||||
VerificationType::Double => RtType::Double,
|
||||
VerificationType::Long => RtType::Long,
|
||||
VerificationType::Null => RtType::Null,
|
||||
VerificationType::UninitializedThis => RtType::UninitializedThis,
|
||||
VerificationType::Object(name) => RtType::Object {
|
||||
cpool_index: pool.class(name)?,
|
||||
},
|
||||
VerificationType::Uninitialized(offset) => RtType::Uninitialized { offset: *offset },
|
||||
})
|
||||
}
|
||||
197
crates/noxis-classfile/src/format/write/code/insns.rs
Normal file
197
crates/noxis-classfile/src/format/write/code/insns.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
//! Наш [`Insn`](crate::insn::Insn) → `Instruction` backend'а.
|
||||
//!
|
||||
//! Обратная сторона `super::super::parse::insns`: метки резолвятся в
|
||||
//! абсолютные номера инструкций (`switch` — в дельты от себя), константы
|
||||
//! и ссылки уходят в пул через [`Pool`](super::super::pool::Pool).
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use ristretto_classfile::attributes::{Instruction as RtInsn, LookupSwitch, TableSwitch};
|
||||
|
||||
use super::super::pool::Pool;
|
||||
use super::ops::{array_load, array_store, array_type, binary, cmp, convert_op, int_const, invoke};
|
||||
use super::{branch, delta, fit_index, ret_op};
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Insn, Label, SlotKind};
|
||||
|
||||
/// Одна инструкция IR → инструкция backend'а (`None` — маркер метки).
|
||||
///
|
||||
/// `index` — номер инструкции в методе (нужен `switch`-дельтам).
|
||||
pub(crate) fn convert(
|
||||
insn: &Insn,
|
||||
index: u32,
|
||||
pool: &mut Pool,
|
||||
resolve: &impl Fn(Label) -> Result<u32, ClassfileError>,
|
||||
) -> Result<Option<RtInsn>, ClassfileError> {
|
||||
Ok(Some(match insn {
|
||||
Insn::Mark(_) => return Ok(None),
|
||||
Insn::Nop => RtInsn::Nop,
|
||||
Insn::AconstNull => RtInsn::Aconst_null,
|
||||
Insn::Const(value) => int_const(value, pool)?,
|
||||
Insn::Load { slot, kind } => load(*slot, *kind),
|
||||
Insn::Store { slot, kind } => store(*slot, *kind),
|
||||
Insn::Iinc { index, delta } => iinc(*index, *delta)?,
|
||||
Insn::ArrayLoad(kind) => array_load(*kind),
|
||||
Insn::ArrayStore(kind) => array_store(*kind),
|
||||
Insn::Pop => RtInsn::Pop,
|
||||
Insn::Pop2 => RtInsn::Pop2,
|
||||
Insn::Dup => RtInsn::Dup,
|
||||
Insn::DupX1 => RtInsn::Dup_x1,
|
||||
Insn::DupX2 => RtInsn::Dup_x2,
|
||||
Insn::Dup2 => RtInsn::Dup2,
|
||||
Insn::Dup2X1 => RtInsn::Dup2_x1,
|
||||
Insn::Dup2X2 => RtInsn::Dup2_x2,
|
||||
Insn::Swap => RtInsn::Swap,
|
||||
Insn::Binary { prim, op } => binary(*prim, *op)?,
|
||||
Insn::Cmp { prim, nan_great } => cmp(*prim, *nan_great)?,
|
||||
Insn::Convert(op) => convert_op(*op),
|
||||
Insn::Branch(cond, target) => branch(*cond, resolve(*target)?)?,
|
||||
Insn::Goto(target) => RtInsn::Goto(fit_index(resolve(*target)?)?),
|
||||
Insn::Jsr(target) => RtInsn::Jsr(fit_index(resolve(*target)?)?),
|
||||
Insn::Ret(slot) => ret(*slot),
|
||||
Insn::TableSwitch {
|
||||
default,
|
||||
low,
|
||||
high,
|
||||
targets,
|
||||
} => RtInsn::Tableswitch(Box::new(TableSwitch {
|
||||
default: delta(index, resolve(*default)?)?,
|
||||
low: *low,
|
||||
high: *high,
|
||||
offsets: targets
|
||||
.iter()
|
||||
.map(|target| delta(index, resolve(*target)?))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})),
|
||||
Insn::LookupSwitch { default, pairs } => RtInsn::Lookupswitch(Box::new(LookupSwitch {
|
||||
default: delta(index, resolve(*default)?)?,
|
||||
pairs: pairs
|
||||
.iter()
|
||||
.map(|(key, target)| Ok((*key, delta(index, resolve(*target)?)?)))
|
||||
.collect::<Result<IndexMap<_, _>, ClassfileError>>()?,
|
||||
})),
|
||||
Insn::Return(prim) => ret_op(*prim),
|
||||
Insn::AReturn => RtInsn::Areturn,
|
||||
Insn::GetStatic {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
} => RtInsn::Getstatic(pool.field_ref(owner, name, descriptor)?),
|
||||
Insn::PutStatic {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
} => RtInsn::Putstatic(pool.field_ref(owner, name, descriptor)?),
|
||||
Insn::GetField {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
} => RtInsn::Getfield(pool.field_ref(owner, name, descriptor)?),
|
||||
Insn::PutField {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
} => RtInsn::Putfield(pool.field_ref(owner, name, descriptor)?),
|
||||
Insn::Invoke {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
..
|
||||
} => invoke(*kind, owner, name, descriptor, pool)?,
|
||||
Insn::InvokeDynamic {
|
||||
name,
|
||||
descriptor,
|
||||
bootstrap,
|
||||
} => RtInsn::Invokedynamic(pool.invoke_dynamic(*bootstrap, name, descriptor)?),
|
||||
Insn::New(name) => RtInsn::New(pool.class(name)?),
|
||||
Insn::NewArray(kind) => RtInsn::Newarray(array_type(*kind)?),
|
||||
Insn::ANewArray(name) => RtInsn::Anewarray(pool.class(name)?),
|
||||
Insn::MultiANewArray { descriptor, dims } => {
|
||||
RtInsn::Multianewarray(pool.class(descriptor)?, *dims)
|
||||
}
|
||||
Insn::ArrayLength => RtInsn::Arraylength,
|
||||
Insn::AThrow => RtInsn::Athrow,
|
||||
Insn::CheckCast(name) => RtInsn::Checkcast(pool.class(name)?),
|
||||
Insn::InstanceOf(name) => RtInsn::Instanceof(pool.class(name)?),
|
||||
Insn::MonitorEnter => RtInsn::Monitorenter,
|
||||
Insn::MonitorExit => RtInsn::Monitorexit,
|
||||
}))
|
||||
}
|
||||
|
||||
fn load(slot: u16, kind: SlotKind) -> RtInsn {
|
||||
match (kind, slot) {
|
||||
(SlotKind::Int, 0) => RtInsn::Iload_0,
|
||||
(SlotKind::Int, 1) => RtInsn::Iload_1,
|
||||
(SlotKind::Int, 2) => RtInsn::Iload_2,
|
||||
(SlotKind::Int, 3) => RtInsn::Iload_3,
|
||||
(SlotKind::Long, 0) => RtInsn::Lload_0,
|
||||
(SlotKind::Long, 1) => RtInsn::Lload_1,
|
||||
(SlotKind::Long, 2) => RtInsn::Lload_2,
|
||||
(SlotKind::Long, 3) => RtInsn::Lload_3,
|
||||
(SlotKind::Float, 0) => RtInsn::Fload_0,
|
||||
(SlotKind::Float, 1) => RtInsn::Fload_1,
|
||||
(SlotKind::Float, 2) => RtInsn::Fload_2,
|
||||
(SlotKind::Float, 3) => RtInsn::Fload_3,
|
||||
(SlotKind::Double, 0) => RtInsn::Dload_0,
|
||||
(SlotKind::Double, 1) => RtInsn::Dload_1,
|
||||
(SlotKind::Double, 2) => RtInsn::Dload_2,
|
||||
(SlotKind::Double, 3) => RtInsn::Dload_3,
|
||||
(SlotKind::Ref, 0) => RtInsn::Aload_0,
|
||||
(SlotKind::Ref, 1) => RtInsn::Aload_1,
|
||||
(SlotKind::Ref, 2) => RtInsn::Aload_2,
|
||||
(SlotKind::Ref, 3) => RtInsn::Aload_3,
|
||||
(SlotKind::Int, s) => narrow(s, RtInsn::Iload, RtInsn::Iload_w),
|
||||
(SlotKind::Long, s) => narrow(s, RtInsn::Lload, RtInsn::Lload_w),
|
||||
(SlotKind::Float, s) => narrow(s, RtInsn::Fload, RtInsn::Fload_w),
|
||||
(SlotKind::Double, s) => narrow(s, RtInsn::Dload, RtInsn::Dload_w),
|
||||
(SlotKind::Ref, s) => narrow(s, RtInsn::Aload, RtInsn::Aload_w),
|
||||
}
|
||||
}
|
||||
|
||||
fn store(slot: u16, kind: SlotKind) -> RtInsn {
|
||||
match (kind, slot) {
|
||||
(SlotKind::Int, 0) => RtInsn::Istore_0,
|
||||
(SlotKind::Int, 1) => RtInsn::Istore_1,
|
||||
(SlotKind::Int, 2) => RtInsn::Istore_2,
|
||||
(SlotKind::Int, 3) => RtInsn::Istore_3,
|
||||
(SlotKind::Long, 0) => RtInsn::Lstore_0,
|
||||
(SlotKind::Long, 1) => RtInsn::Lstore_1,
|
||||
(SlotKind::Long, 2) => RtInsn::Lstore_2,
|
||||
(SlotKind::Long, 3) => RtInsn::Lstore_3,
|
||||
(SlotKind::Float, 0) => RtInsn::Fstore_0,
|
||||
(SlotKind::Float, 1) => RtInsn::Fstore_1,
|
||||
(SlotKind::Float, 2) => RtInsn::Fstore_2,
|
||||
(SlotKind::Float, 3) => RtInsn::Fstore_3,
|
||||
(SlotKind::Double, 0) => RtInsn::Dstore_0,
|
||||
(SlotKind::Double, 1) => RtInsn::Dstore_1,
|
||||
(SlotKind::Double, 2) => RtInsn::Dstore_2,
|
||||
(SlotKind::Double, 3) => RtInsn::Dstore_3,
|
||||
(SlotKind::Ref, 0) => RtInsn::Astore_0,
|
||||
(SlotKind::Ref, 1) => RtInsn::Astore_1,
|
||||
(SlotKind::Ref, 2) => RtInsn::Astore_2,
|
||||
(SlotKind::Ref, 3) => RtInsn::Astore_3,
|
||||
(SlotKind::Int, s) => narrow(s, RtInsn::Istore, RtInsn::Istore_w),
|
||||
(SlotKind::Long, s) => narrow(s, RtInsn::Lstore, RtInsn::Lstore_w),
|
||||
(SlotKind::Float, s) => narrow(s, RtInsn::Fstore, RtInsn::Fstore_w),
|
||||
(SlotKind::Double, s) => narrow(s, RtInsn::Dstore, RtInsn::Dstore_w),
|
||||
(SlotKind::Ref, s) => narrow(s, RtInsn::Astore, RtInsn::Astore_w),
|
||||
}
|
||||
}
|
||||
|
||||
/// Короткая форма при слоте ≤255, иначе `wide`-форма.
|
||||
fn narrow(slot: u16, short: fn(u8) -> RtInsn, wide: fn(u16) -> RtInsn) -> RtInsn {
|
||||
u8::try_from(slot).map(short).unwrap_or_else(|_| wide(slot))
|
||||
}
|
||||
|
||||
fn ret(slot: u16) -> RtInsn {
|
||||
narrow(slot, RtInsn::Ret, RtInsn::Ret_w)
|
||||
}
|
||||
|
||||
fn iinc(index: u16, delta: i32) -> Result<RtInsn, ClassfileError> {
|
||||
if let (Ok(i), Ok(d)) = (u8::try_from(index), i8::try_from(delta)) {
|
||||
return Ok(RtInsn::Iinc(i, d));
|
||||
}
|
||||
let wide_delta = i16::try_from(delta)
|
||||
.map_err(|_| ClassfileError::Write(format!("приращение iinc {delta} не влезает в i16")))?;
|
||||
Ok(RtInsn::Iinc_w(index, wide_delta))
|
||||
}
|
||||
231
crates/noxis-classfile/src/format/write/code/mod.rs
Normal file
231
crates/noxis-classfile/src/format/write/code/mod.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
//! Тело метода нашего IR → атрибут `Code` backend'а.
|
||||
//!
|
||||
//! Метки резолвятся в номера инструкций один раз здесь; дальше backend сам
|
||||
//! пересчитывает номера в байтовые смещения при `to_bytes`.
|
||||
|
||||
mod frames;
|
||||
pub(crate) mod insns;
|
||||
mod ops;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Cursor;
|
||||
|
||||
use ristretto_classfile::attributes::{
|
||||
Attribute as RtAttr, ExceptionTableEntry as RtEntry, Instruction as RtInsn,
|
||||
};
|
||||
|
||||
use super::attrs;
|
||||
use super::pool::Pool;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
|
||||
use crate::model::{Attribute, Code, ExceptionHandler};
|
||||
|
||||
use frames::convert_frames;
|
||||
|
||||
/// Тело метода → готовый атрибут `Code` (с логическими номерами инструкций).
|
||||
pub(super) fn convert_code(code: &Code, pool: &mut Pool) -> Result<RtAttr, ClassfileError> {
|
||||
let (labels, bare) = split_marks(&code.insns)?;
|
||||
let resolve = |label: Label| -> Result<u32, ClassfileError> {
|
||||
labels
|
||||
.get(&label)
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Write(format!("нет метки {label}")))
|
||||
};
|
||||
let mut rt_insns = Vec::with_capacity(bare.len());
|
||||
for (index, insn) in bare.iter().enumerate() {
|
||||
let index = u32::try_from(index)
|
||||
.map_err(|_| ClassfileError::Write("в методе больше 4 млрд инструкций".into()))?;
|
||||
if let Some(rt) = insns::convert(insn, index, pool, &resolve)? {
|
||||
rt_insns.push(rt);
|
||||
}
|
||||
}
|
||||
let exception_table = code
|
||||
.exception_table
|
||||
.iter()
|
||||
.map(|handler| entry(handler, &resolve, pool))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let to_byte = measure(&rt_insns)?;
|
||||
let byte_of = |index: u32| -> Result<u16, ClassfileError> {
|
||||
to_byte
|
||||
.get(&index)
|
||||
.copied()
|
||||
.ok_or_else(|| ClassfileError::Write(format!("нет байта для инструкции {index}")))
|
||||
};
|
||||
let positions = attrs::Positions {
|
||||
resolve: &resolve,
|
||||
byte_of: &byte_of,
|
||||
};
|
||||
let frames = convert_frames(&code.frames, pool)?;
|
||||
let mut attributes = Vec::with_capacity(code.attributes.len() + 1);
|
||||
for attr in &code.attributes {
|
||||
attributes.push(match attr {
|
||||
// Backend хранит номера строк в индексах и маппит сам.
|
||||
Attribute::LineNumberTable(lines) => attrs::line_number_table(lines, pool, &positions)?,
|
||||
Attribute::LocalVariableTable(vars) => attrs::local_table(vars, pool, &positions)?,
|
||||
Attribute::LocalVariableTypeTable(vars) => {
|
||||
attrs::local_type_table(vars, pool, &positions)?
|
||||
}
|
||||
other => attrs::convert(other, pool)?,
|
||||
});
|
||||
}
|
||||
if !frames.is_empty() {
|
||||
attributes.push(RtAttr::StackMapTable {
|
||||
name_index: pool.utf8("StackMapTable")?,
|
||||
frames,
|
||||
});
|
||||
}
|
||||
Ok(RtAttr::Code {
|
||||
name_index: pool.utf8("Code")?,
|
||||
max_stack: code.max_stack,
|
||||
max_locals: code.max_locals,
|
||||
code: rt_insns,
|
||||
exception_table,
|
||||
attributes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Метки → номера инструкций; сами маркеры из потока убираются.
|
||||
fn split_marks(insns: &[Insn]) -> Result<(BTreeMap<Label, u32>, Vec<&Insn>), ClassfileError> {
|
||||
let mut labels = BTreeMap::new();
|
||||
let mut bare = Vec::with_capacity(insns.len());
|
||||
for insn in insns {
|
||||
match insn {
|
||||
Insn::Mark(label) => {
|
||||
let index = u32::try_from(bare.len()).map_err(|_| {
|
||||
ClassfileError::Write("в методе больше 4 млрд инструкций".into())
|
||||
})?;
|
||||
if let Some(prev) = labels.insert(*label, index)
|
||||
&& prev != index
|
||||
{
|
||||
return Err(ClassfileError::Write(format!(
|
||||
"метка {label} объявлена дважды"
|
||||
)));
|
||||
}
|
||||
}
|
||||
_ => bare.push(insn),
|
||||
}
|
||||
}
|
||||
Ok((labels, bare))
|
||||
}
|
||||
|
||||
/// Номер инструкции → байтовое смещение (замер с заглушками целей).
|
||||
///
|
||||
/// В карту входит и конец кода (`len` → длина): концы диапазонов указывают
|
||||
/// за последнюю инструкцию.
|
||||
pub(crate) fn measure(insns: &[RtInsn]) -> Result<BTreeMap<u32, u16>, ClassfileError> {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
for (index, insn) in insns.iter().enumerate() {
|
||||
let position = tell(&cursor)?;
|
||||
let index = u32::try_from(index)
|
||||
.map_err(|_| ClassfileError::Write("в методе больше 4 млрд инструкций".into()))?;
|
||||
map.insert(index, position);
|
||||
let mut sizing = insn.clone();
|
||||
smash_targets(&mut sizing, position);
|
||||
sizing
|
||||
.to_bytes(&mut cursor)
|
||||
.map_err(|e| ClassfileError::Write(e.to_string()))?;
|
||||
}
|
||||
let end = u32::try_from(insns.len())
|
||||
.map_err(|_| ClassfileError::Write("в методе больше 4 млрд инструкций".into()))?;
|
||||
map.insert(end, tell(&cursor)?);
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Цели ветвлений не влияют на размер — подменяем текущей позицией для замера.
|
||||
fn smash_targets(insn: &mut RtInsn, position: u16) {
|
||||
match insn {
|
||||
RtInsn::Ifeq(target)
|
||||
| RtInsn::Ifne(target)
|
||||
| RtInsn::Iflt(target)
|
||||
| RtInsn::Ifge(target)
|
||||
| RtInsn::Ifgt(target)
|
||||
| RtInsn::Ifle(target)
|
||||
| RtInsn::If_icmpeq(target)
|
||||
| RtInsn::If_icmpne(target)
|
||||
| RtInsn::If_icmplt(target)
|
||||
| RtInsn::If_icmpge(target)
|
||||
| RtInsn::If_icmpgt(target)
|
||||
| RtInsn::If_icmple(target)
|
||||
| RtInsn::If_acmpeq(target)
|
||||
| RtInsn::If_acmpne(target)
|
||||
| RtInsn::Goto(target)
|
||||
| RtInsn::Jsr(target)
|
||||
| RtInsn::Ifnull(target)
|
||||
| RtInsn::Ifnonnull(target) => *target = position,
|
||||
RtInsn::Goto_w(target) | RtInsn::Jsr_w(target) => *target = i32::from(position),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn tell(cursor: &Cursor<Vec<u8>>) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(cursor.position())
|
||||
.map_err(|_| ClassfileError::Write("тело метода длиннее 64 КБ".into()))
|
||||
}
|
||||
|
||||
fn entry(
|
||||
handler: &ExceptionHandler,
|
||||
resolve: &impl Fn(Label) -> Result<u32, ClassfileError>,
|
||||
pool: &mut Pool,
|
||||
) -> Result<RtEntry, ClassfileError> {
|
||||
Ok(RtEntry {
|
||||
range_pc: fit_index(resolve(handler.start)?)?..fit_index(resolve(handler.end)?)?,
|
||||
handler_pc: fit_index(resolve(handler.handler)?)?,
|
||||
catch_type: match &handler.catch_type {
|
||||
None => 0,
|
||||
Some(name) => pool.class(name)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Условный переход IR → `if*` backend'а.
|
||||
fn branch(cond: BranchCond, target: u32) -> Result<RtInsn, ClassfileError> {
|
||||
let target = fit_index(target)?;
|
||||
Ok(match cond {
|
||||
BranchCond::Zero(Cond::Eq) => RtInsn::Ifeq(target),
|
||||
BranchCond::Zero(Cond::Ne) => RtInsn::Ifne(target),
|
||||
BranchCond::Zero(Cond::Lt) => RtInsn::Iflt(target),
|
||||
BranchCond::Zero(Cond::Ge) => RtInsn::Ifge(target),
|
||||
BranchCond::Zero(Cond::Gt) => RtInsn::Ifgt(target),
|
||||
BranchCond::Zero(Cond::Le) => RtInsn::Ifle(target),
|
||||
BranchCond::Int(Cond::Eq) => RtInsn::If_icmpeq(target),
|
||||
BranchCond::Int(Cond::Ne) => RtInsn::If_icmpne(target),
|
||||
BranchCond::Int(Cond::Lt) => RtInsn::If_icmplt(target),
|
||||
BranchCond::Int(Cond::Ge) => RtInsn::If_icmpge(target),
|
||||
BranchCond::Int(Cond::Gt) => RtInsn::If_icmpgt(target),
|
||||
BranchCond::Int(Cond::Le) => RtInsn::If_icmple(target),
|
||||
BranchCond::Ref { eq: true } => RtInsn::If_acmpeq(target),
|
||||
BranchCond::Ref { eq: false } => RtInsn::If_acmpne(target),
|
||||
BranchCond::Null { is_null: true } => RtInsn::Ifnull(target),
|
||||
BranchCond::Null { is_null: false } => RtInsn::Ifnonnull(target),
|
||||
})
|
||||
}
|
||||
|
||||
fn ret_op(prim: Option<Prim>) -> RtInsn {
|
||||
match prim {
|
||||
None => RtInsn::Return,
|
||||
Some(Prim::Int) => RtInsn::Ireturn,
|
||||
Some(Prim::Long) => RtInsn::Lreturn,
|
||||
Some(Prim::Float) => RtInsn::Freturn,
|
||||
Some(Prim::Double) => RtInsn::Dreturn,
|
||||
}
|
||||
}
|
||||
|
||||
/// Номер инструкции обязан влезать в `u16` (метод < 64 КБ кода).
|
||||
fn fit_index(index: u32) -> Result<u16, ClassfileError> {
|
||||
u16::try_from(index)
|
||||
.map_err(|_| ClassfileError::Write(format!("номер инструкции {index} вне u16")))
|
||||
}
|
||||
|
||||
/// Дельта `switch`: цель минус позиция самого `switch`.
|
||||
fn delta(index: u32, target: u32) -> Result<i32, ClassfileError> {
|
||||
let (index, target) = (to_i32(index)?, to_i32(target)?);
|
||||
target
|
||||
.checked_sub(index)
|
||||
.ok_or_else(|| ClassfileError::Write(format!("переполнение дельты {index}->{target}")))
|
||||
}
|
||||
|
||||
fn to_i32(value: u32) -> Result<i32, ClassfileError> {
|
||||
i32::try_from(value)
|
||||
.map_err(|_| ClassfileError::Write(format!("номер инструкции {value} вне i32")))
|
||||
}
|
||||
244
crates/noxis-classfile/src/format/write/code/ops.rs
Normal file
244
crates/noxis-classfile/src/format/write/code/ops.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
//! Операнды: константы, арифметика, массивы, вызовы.
|
||||
|
||||
use ristretto_classfile::attributes::{ArrayType, Instruction as RtInsn};
|
||||
|
||||
use super::super::pool::Pool;
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Arith, ArrayKind, Const, Convert, InvokeKind, Prim};
|
||||
|
||||
/// Константа: короткие формы (`iconst`, `bipush`) + `ldc_w`/`ldc2_w` через пул.
|
||||
pub(super) fn int_const(value: &Const, pool: &mut Pool) -> Result<RtInsn, ClassfileError> {
|
||||
Ok(match value {
|
||||
Const::Int(v) => match *v {
|
||||
-1 => RtInsn::Iconst_m1,
|
||||
0 => RtInsn::Iconst_0,
|
||||
1 => RtInsn::Iconst_1,
|
||||
2 => RtInsn::Iconst_2,
|
||||
3 => RtInsn::Iconst_3,
|
||||
4 => RtInsn::Iconst_4,
|
||||
5 => RtInsn::Iconst_5,
|
||||
v if i8::try_from(v).is_ok() => RtInsn::Bipush(v as i8),
|
||||
v if i16::try_from(v).is_ok() => RtInsn::Sipush(v as i16),
|
||||
_ => RtInsn::Ldc_w(pool.constant(value)?),
|
||||
},
|
||||
Const::Long(0) => RtInsn::Lconst_0,
|
||||
Const::Long(1) => RtInsn::Lconst_1,
|
||||
Const::Long(_) => RtInsn::Ldc2_w(pool.constant(value)?),
|
||||
Const::Float(v) if *v == 0.0 => RtInsn::Fconst_0,
|
||||
Const::Float(v) if *v == 1.0 => RtInsn::Fconst_1,
|
||||
Const::Float(v) if *v == 2.0 => RtInsn::Fconst_2,
|
||||
Const::Float(_) => RtInsn::Ldc_w(pool.constant(value)?),
|
||||
Const::Double(v) if *v == 0.0 => RtInsn::Dconst_0,
|
||||
Const::Double(v) if *v == 1.0 => RtInsn::Dconst_1,
|
||||
Const::Double(_) => RtInsn::Ldc2_w(pool.constant(value)?),
|
||||
Const::Dynamic {
|
||||
name,
|
||||
descriptor,
|
||||
bootstrap,
|
||||
} => RtInsn::Ldc2_w(pool.dynamic(*bootstrap, name, descriptor)?),
|
||||
_ => RtInsn::Ldc_w(pool.constant(value)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn array_load(kind: ArrayKind) -> RtInsn {
|
||||
match kind {
|
||||
ArrayKind::Boolean | ArrayKind::Byte => RtInsn::Baload,
|
||||
ArrayKind::Char => RtInsn::Caload,
|
||||
ArrayKind::Short => RtInsn::Saload,
|
||||
ArrayKind::Int => RtInsn::Iaload,
|
||||
ArrayKind::Long => RtInsn::Laload,
|
||||
ArrayKind::Float => RtInsn::Faload,
|
||||
ArrayKind::Double => RtInsn::Daload,
|
||||
ArrayKind::Reference => RtInsn::Aaload,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn array_store(kind: ArrayKind) -> RtInsn {
|
||||
match kind {
|
||||
ArrayKind::Boolean | ArrayKind::Byte => RtInsn::Bastore,
|
||||
ArrayKind::Char => RtInsn::Castore,
|
||||
ArrayKind::Short => RtInsn::Sastore,
|
||||
ArrayKind::Int => RtInsn::Iastore,
|
||||
ArrayKind::Long => RtInsn::Lastore,
|
||||
ArrayKind::Float => RtInsn::Fastore,
|
||||
ArrayKind::Double => RtInsn::Dastore,
|
||||
ArrayKind::Reference => RtInsn::Aastore,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn array_type(kind: ArrayKind) -> Result<ArrayType, ClassfileError> {
|
||||
Ok(match kind {
|
||||
ArrayKind::Boolean => ArrayType::Boolean,
|
||||
ArrayKind::Char => ArrayType::Char,
|
||||
ArrayKind::Float => ArrayType::Float,
|
||||
ArrayKind::Double => ArrayType::Double,
|
||||
ArrayKind::Byte => ArrayType::Byte,
|
||||
ArrayKind::Short => ArrayType::Short,
|
||||
ArrayKind::Int => ArrayType::Int,
|
||||
ArrayKind::Long => ArrayType::Long,
|
||||
ArrayKind::Reference => {
|
||||
return Err(ClassfileError::Write(
|
||||
"newarray не создаёт ссылочные массивы (нужен anewarray)".into(),
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn binary(prim: Prim, op: Arith) -> Result<RtInsn, ClassfileError> {
|
||||
use Arith::{Add, And, Div, Mul, Neg, Or, Rem, Shl, Shr, Sub, Ushr, Xor};
|
||||
Ok(match (prim, op) {
|
||||
(Prim::Int, Add) => RtInsn::Iadd,
|
||||
(Prim::Int, Sub) => RtInsn::Isub,
|
||||
(Prim::Int, Mul) => RtInsn::Imul,
|
||||
(Prim::Int, Div) => RtInsn::Idiv,
|
||||
(Prim::Int, Rem) => RtInsn::Irem,
|
||||
(Prim::Int, Neg) => RtInsn::Ineg,
|
||||
(Prim::Int, And) => RtInsn::Iand,
|
||||
(Prim::Int, Or) => RtInsn::Ior,
|
||||
(Prim::Int, Xor) => RtInsn::Ixor,
|
||||
(Prim::Int, Shl) => RtInsn::Ishl,
|
||||
(Prim::Int, Shr) => RtInsn::Ishr,
|
||||
(Prim::Int, Ushr) => RtInsn::Iushr,
|
||||
(Prim::Long, Add) => RtInsn::Ladd,
|
||||
(Prim::Long, Sub) => RtInsn::Lsub,
|
||||
(Prim::Long, Mul) => RtInsn::Lmul,
|
||||
(Prim::Long, Div) => RtInsn::Ldiv,
|
||||
(Prim::Long, Rem) => RtInsn::Lrem,
|
||||
(Prim::Long, Neg) => RtInsn::Lneg,
|
||||
(Prim::Long, And) => RtInsn::Land,
|
||||
(Prim::Long, Or) => RtInsn::Lor,
|
||||
(Prim::Long, Xor) => RtInsn::Lxor,
|
||||
(Prim::Long, Shl) => RtInsn::Lshl,
|
||||
(Prim::Long, Shr) => RtInsn::Lshr,
|
||||
(Prim::Long, Ushr) => RtInsn::Lushr,
|
||||
(Prim::Float, Add) => RtInsn::Fadd,
|
||||
(Prim::Float, Sub) => RtInsn::Fsub,
|
||||
(Prim::Float, Mul) => RtInsn::Fmul,
|
||||
(Prim::Float, Div) => RtInsn::Fdiv,
|
||||
(Prim::Float, Rem) => RtInsn::Frem,
|
||||
(Prim::Float, Neg) => RtInsn::Fneg,
|
||||
(Prim::Double, Add) => RtInsn::Dadd,
|
||||
(Prim::Double, Sub) => RtInsn::Dsub,
|
||||
(Prim::Double, Mul) => RtInsn::Dmul,
|
||||
(Prim::Double, Div) => RtInsn::Ddiv,
|
||||
(Prim::Double, Rem) => RtInsn::Drem,
|
||||
(Prim::Double, Neg) => RtInsn::Dneg,
|
||||
(prim, op) => {
|
||||
return Err(ClassfileError::Write(format!(
|
||||
"недопустимая комбинация {prim:?}/{op:?}"
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn cmp(prim: Prim, nan_great: bool) -> Result<RtInsn, ClassfileError> {
|
||||
Ok(match (prim, nan_great) {
|
||||
(Prim::Long, false) => RtInsn::Lcmp,
|
||||
(Prim::Float, false) => RtInsn::Fcmpl,
|
||||
(Prim::Float, true) => RtInsn::Fcmpg,
|
||||
(Prim::Double, false) => RtInsn::Dcmpl,
|
||||
(Prim::Double, true) => RtInsn::Dcmpg,
|
||||
(prim, nan_great) => {
|
||||
return Err(ClassfileError::Write(format!(
|
||||
"недопустимое сравнение {prim:?}/{nan_great}"
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn convert_op(op: Convert) -> RtInsn {
|
||||
match op {
|
||||
Convert::I2L => RtInsn::I2l,
|
||||
Convert::I2F => RtInsn::I2f,
|
||||
Convert::I2D => RtInsn::I2d,
|
||||
Convert::I2B => RtInsn::I2b,
|
||||
Convert::I2C => RtInsn::I2c,
|
||||
Convert::I2S => RtInsn::I2s,
|
||||
Convert::L2I => RtInsn::L2i,
|
||||
Convert::L2F => RtInsn::L2f,
|
||||
Convert::L2D => RtInsn::L2d,
|
||||
Convert::F2I => RtInsn::F2i,
|
||||
Convert::F2L => RtInsn::F2l,
|
||||
Convert::F2D => RtInsn::F2d,
|
||||
Convert::D2I => RtInsn::D2i,
|
||||
Convert::D2L => RtInsn::D2l,
|
||||
Convert::D2F => RtInsn::D2f,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn invoke(
|
||||
kind: InvokeKind,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
pool: &mut Pool,
|
||||
) -> Result<RtInsn, ClassfileError> {
|
||||
Ok(match kind {
|
||||
InvokeKind::Virtual => {
|
||||
RtInsn::Invokevirtual(pool.method_ref(owner, name, descriptor, kind)?)
|
||||
}
|
||||
InvokeKind::Special => {
|
||||
RtInsn::Invokespecial(pool.method_ref(owner, name, descriptor, kind)?)
|
||||
}
|
||||
InvokeKind::Static => RtInsn::Invokestatic(pool.method_ref(owner, name, descriptor, kind)?),
|
||||
InvokeKind::Interface => {
|
||||
let index = pool.method_ref(owner, name, descriptor, kind)?;
|
||||
RtInsn::Invokeinterface(index, arg_count(descriptor)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Байт `count` для `invokeinterface`: слоты аргументов + `objectref`.
|
||||
fn arg_count(descriptor: &str) -> Result<u8, ClassfileError> {
|
||||
let params = descriptor
|
||||
.split_once('(')
|
||||
.and_then(|(_, rest)| rest.split_once(')'))
|
||||
.map(|(params, _)| params)
|
||||
.ok_or_else(|| ClassfileError::Write(format!("кривой дескриптор {descriptor}")))?;
|
||||
let mut slots = 1u32;
|
||||
let mut chars = params.chars();
|
||||
while let Some(char) = chars.next() {
|
||||
match char {
|
||||
'B' | 'C' | 'F' | 'I' | 'S' | 'Z' => slots += 1,
|
||||
'J' | 'D' => slots += 2,
|
||||
'L' => {
|
||||
if !skip_to(chars.by_ref(), ';') {
|
||||
return Err(bad_descriptor(descriptor));
|
||||
}
|
||||
slots += 1;
|
||||
}
|
||||
'[' => {
|
||||
while chars.as_str().starts_with('[') {
|
||||
chars.next();
|
||||
}
|
||||
match chars.next() {
|
||||
Some('L') => {
|
||||
if !skip_to(chars.by_ref(), ';') {
|
||||
return Err(bad_descriptor(descriptor));
|
||||
}
|
||||
}
|
||||
Some(_) => {}
|
||||
None => return Err(bad_descriptor(descriptor)),
|
||||
}
|
||||
slots += 1;
|
||||
}
|
||||
_ => return Err(bad_descriptor(descriptor)),
|
||||
}
|
||||
}
|
||||
u8::try_from(slots)
|
||||
.map_err(|_| ClassfileError::Write(format!("слишком много аргументов в {descriptor}")))
|
||||
}
|
||||
|
||||
/// Пропустить символы до разделителя (включительно); `false` — строка кончилась.
|
||||
fn skip_to(chars: &mut std::str::Chars<'_>, end: char) -> bool {
|
||||
for char in chars {
|
||||
if char == end {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn bad_descriptor(descriptor: &str) -> ClassfileError {
|
||||
ClassfileError::Write(format!("кривой дескриптор {descriptor}"))
|
||||
}
|
||||
93
crates/noxis-classfile/src/format/write/mod.rs
Normal file
93
crates/noxis-classfile/src/format/write/mod.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
//! Наш IR → байты `.class` через backend `ristretto_classfile`.
|
||||
//!
|
||||
//! Пул собирается с нуля детерминированным обходом; backend сам пересчитывает
|
||||
//! номера инструкций в байтовые смещения и проверяет результат (`verify`).
|
||||
|
||||
mod attrs;
|
||||
pub(crate) mod code;
|
||||
pub(crate) mod pool;
|
||||
|
||||
use ristretto_classfile::{
|
||||
ClassAccessFlags, ClassFile as RtClass, Field as RtField, FieldAccessFlags, FieldType,
|
||||
Method as RtMethod, MethodAccessFlags, Version as RtVersion,
|
||||
};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::model::{ClassFile, Field, Method};
|
||||
|
||||
/// Класс нашего IR → байты `.class`.
|
||||
pub(crate) fn write_class(class: &ClassFile) -> Result<Vec<u8>, ClassfileError> {
|
||||
let mut pool = pool::Pool::new();
|
||||
let version = RtVersion::from(class.version.major, class.version.minor)
|
||||
.map_err(|e| ClassfileError::Write(e.to_string()))?;
|
||||
let this_class = pool.class(&class.this_class)?;
|
||||
let super_class = match &class.super_class {
|
||||
None => 0,
|
||||
Some(name) => pool.class(name)?,
|
||||
};
|
||||
let mut interfaces = Vec::with_capacity(class.interfaces.len());
|
||||
for interface in &class.interfaces {
|
||||
interfaces.push(pool.class(interface)?);
|
||||
}
|
||||
let mut fields = Vec::with_capacity(class.fields.len());
|
||||
for field in &class.fields {
|
||||
fields.push(convert_field(field, &mut pool)?);
|
||||
}
|
||||
let mut methods = Vec::with_capacity(class.methods.len());
|
||||
for method in &class.methods {
|
||||
methods.push(convert_method(method, &mut pool)?);
|
||||
}
|
||||
let mut attributes = Vec::with_capacity(class.attributes.len());
|
||||
for attr in &class.attributes {
|
||||
attributes.push(attrs::convert(attr, &mut pool)?);
|
||||
}
|
||||
let rt = RtClass {
|
||||
version,
|
||||
constant_pool: pool.finish(),
|
||||
access_flags: ClassAccessFlags::from_bits_truncate(class.access_flags),
|
||||
this_class,
|
||||
super_class,
|
||||
interfaces,
|
||||
fields,
|
||||
methods,
|
||||
attributes,
|
||||
code_source_url: None,
|
||||
};
|
||||
rt.verify()
|
||||
.map_err(|e| ClassfileError::Write(format!("собранный класс не верифицируется: {e}")))?;
|
||||
let mut bytes = Vec::new();
|
||||
rt.to_bytes(&mut bytes)
|
||||
.map_err(|e| ClassfileError::Write(e.to_string()))?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn convert_field(field: &Field, pool: &mut pool::Pool) -> Result<RtField, ClassfileError> {
|
||||
Ok(RtField {
|
||||
access_flags: FieldAccessFlags::from_bits_truncate(field.access_flags),
|
||||
name_index: pool.utf8(&field.name)?,
|
||||
descriptor_index: pool.utf8(&field.descriptor)?,
|
||||
field_type: FieldType::parse(&field.descriptor)
|
||||
.map_err(|e| ClassfileError::Write(format!("кривой дескриптор поля: {e}")))?,
|
||||
attributes: field
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attr| attrs::convert(attr, pool))
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_method(method: &Method, pool: &mut pool::Pool) -> Result<RtMethod, ClassfileError> {
|
||||
let mut attributes = Vec::with_capacity(method.attributes.len() + 1);
|
||||
for attr in &method.attributes {
|
||||
attributes.push(attrs::convert(attr, pool)?);
|
||||
}
|
||||
if let Some(code) = &method.code {
|
||||
attributes.push(code::convert_code(code, pool)?);
|
||||
}
|
||||
Ok(RtMethod {
|
||||
access_flags: MethodAccessFlags::from_bits_truncate(method.access_flags),
|
||||
name_index: pool.utf8(&method.name)?,
|
||||
descriptor_index: pool.utf8(&method.descriptor)?,
|
||||
attributes,
|
||||
})
|
||||
}
|
||||
155
crates/noxis-classfile/src/format/write/pool.rs
Normal file
155
crates/noxis-classfile/src/format/write/pool.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
//! Сборка constant pool при записи: строки нашего IR → индексы backend'а.
|
||||
//!
|
||||
//! Пул строится с нуля детерминированным обходом (классы, поля, методы,
|
||||
//! атрибуты, код), поэтому одинаковый IR даёт одинаковые байты. Индексы
|
||||
//! `Raw`-атрибутов при этом не переносятся — см. `super::attrs`.
|
||||
|
||||
use ristretto_classfile::{ConstantPool, ReferenceKind};
|
||||
|
||||
use crate::error::ClassfileError;
|
||||
use crate::insn::{Const, InvokeKind};
|
||||
|
||||
/// Строитель пула; живёт только внутри одного `write`.
|
||||
pub(crate) struct Pool {
|
||||
pool: ConstantPool<'static>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub(crate) fn new() -> Self {
|
||||
Pool {
|
||||
pool: ConstantPool::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn utf8(&mut self, value: &str) -> Result<u16, ClassfileError> {
|
||||
self.pool.add_utf8(value).map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn name_and_type(
|
||||
&mut self,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
) -> Result<u16, ClassfileError> {
|
||||
self.pool
|
||||
.add_name_and_type(name, descriptor)
|
||||
.map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn class(&mut self, name: &str) -> Result<u16, ClassfileError> {
|
||||
self.pool.add_class(name).map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn field_ref(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
) -> Result<u16, ClassfileError> {
|
||||
let owner = self.class(owner)?;
|
||||
self.pool
|
||||
.add_field_ref(owner, name, descriptor)
|
||||
.map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn method_ref(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
kind: InvokeKind,
|
||||
) -> Result<u16, ClassfileError> {
|
||||
let owner = self.class(owner)?;
|
||||
match kind {
|
||||
InvokeKind::Interface => self
|
||||
.pool
|
||||
.add_interface_method_ref(owner, name, descriptor)
|
||||
.map_err(write_err),
|
||||
_ => self
|
||||
.pool
|
||||
.add_method_ref(owner, name, descriptor)
|
||||
.map_err(write_err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Константа для `ldc`/`ldc2_w`; `dynamic` запрещён (только через bootstrap).
|
||||
pub(super) fn constant(&mut self, value: &Const) -> Result<u16, ClassfileError> {
|
||||
match value {
|
||||
Const::Int(v) => self.pool.add_integer(*v).map_err(write_err),
|
||||
Const::Float(v) => self.pool.add_float(*v).map_err(write_err),
|
||||
Const::Long(v) => self.pool.add_long(*v).map_err(write_err),
|
||||
Const::Double(v) => self.pool.add_double(*v).map_err(write_err),
|
||||
Const::String(v) => self.pool.add_string(v).map_err(write_err),
|
||||
Const::Class(v) => self.class(v),
|
||||
Const::MethodType(v) => self.pool.add_method_type(v).map_err(write_err),
|
||||
Const::MethodHandle {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
itf,
|
||||
} => self.method_handle(*kind, owner, name, descriptor, *itf),
|
||||
Const::Dynamic { .. } => Err(ClassfileError::Write(
|
||||
"CONSTANT_Dynamic пишется только через BootstrapMethods".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn method_handle(
|
||||
&mut self,
|
||||
kind: u8,
|
||||
owner: &str,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
itf: bool,
|
||||
) -> Result<u16, ClassfileError> {
|
||||
let reference_kind = ReferenceKind::try_from(kind).map_err(|e| {
|
||||
ClassfileError::Write(format!("некорректный reference_kind {kind}: {e}"))
|
||||
})?;
|
||||
let reference = if itf {
|
||||
self.method_ref(owner, name, descriptor, InvokeKind::Interface)?
|
||||
} else if matches!(
|
||||
reference_kind,
|
||||
ReferenceKind::GetField
|
||||
| ReferenceKind::GetStatic
|
||||
| ReferenceKind::PutField
|
||||
| ReferenceKind::PutStatic
|
||||
) {
|
||||
self.field_ref(owner, name, descriptor)?
|
||||
} else {
|
||||
self.method_ref(owner, name, descriptor, InvokeKind::Static)?
|
||||
};
|
||||
self.pool
|
||||
.add_method_handle(reference_kind, reference)
|
||||
.map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn invoke_dynamic(
|
||||
&mut self,
|
||||
bootstrap: u16,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
) -> Result<u16, ClassfileError> {
|
||||
self.pool
|
||||
.add_invoke_dynamic(bootstrap, name, descriptor)
|
||||
.map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn dynamic(
|
||||
&mut self,
|
||||
bootstrap: u16,
|
||||
name: &str,
|
||||
descriptor: &str,
|
||||
) -> Result<u16, ClassfileError> {
|
||||
self.pool
|
||||
.add_dynamic(bootstrap, name, descriptor)
|
||||
.map_err(write_err)
|
||||
}
|
||||
|
||||
pub(super) fn finish(self) -> ConstantPool<'static> {
|
||||
self.pool
|
||||
}
|
||||
}
|
||||
|
||||
fn write_err(error: ristretto_classfile::Error) -> ClassfileError {
|
||||
ClassfileError::Write(error.to_string())
|
||||
}
|
||||
136
crates/noxis-classfile/src/insn/kinds.rs
Normal file
136
crates/noxis-classfile/src/insn/kinds.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
//! Типы операндов инструкций: константы, примитивы, условия, вызовы.
|
||||
|
||||
/// Идентификатор метки внутри одного метода.
|
||||
pub type Label = u32;
|
||||
|
||||
/// Константа для `ldc`/`ldc_w`/`ldc2_w`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Const {
|
||||
Int(i32),
|
||||
Long(i64),
|
||||
Float(f32),
|
||||
Double(f64),
|
||||
String(String),
|
||||
/// Внутреннее имя класса (`java/lang/String`).
|
||||
Class(String),
|
||||
MethodType(String),
|
||||
MethodHandle {
|
||||
kind: u8,
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
itf: bool,
|
||||
},
|
||||
/// `CONSTANT_Dynamic`: имя, дескриптор, индекс bootstrap-метода.
|
||||
Dynamic {
|
||||
name: String,
|
||||
descriptor: String,
|
||||
bootstrap: u16,
|
||||
},
|
||||
}
|
||||
|
||||
/// Размер операнда/результата арифметики.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Prim {
|
||||
Int,
|
||||
Long,
|
||||
Float,
|
||||
Double,
|
||||
}
|
||||
|
||||
/// Арифметическая/битовая операция; тип берётся из [`Prim`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Arith {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Rem,
|
||||
Neg,
|
||||
And,
|
||||
Or,
|
||||
Xor,
|
||||
Shl,
|
||||
Shr,
|
||||
Ushr,
|
||||
}
|
||||
|
||||
/// Условие перехода.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Cond {
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Le,
|
||||
Gt,
|
||||
Ge,
|
||||
}
|
||||
|
||||
/// Вид условного перехода (соответствует семействам `if*`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BranchCond {
|
||||
/// `if<cond>` относительно нуля.
|
||||
Zero(Cond),
|
||||
/// `if_icmp<cond>`.
|
||||
Int(Cond),
|
||||
/// `if_acmp<eq|ne>`.
|
||||
Ref { eq: bool },
|
||||
/// `ifnull` / `ifnonnull`.
|
||||
Null { is_null: bool },
|
||||
}
|
||||
|
||||
/// Вид вызова.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InvokeKind {
|
||||
Virtual,
|
||||
Special,
|
||||
Static,
|
||||
Interface,
|
||||
}
|
||||
|
||||
/// Конверсия типов (`i2l`, `f2b`, ... — JVMS §6.5.i2<b|c|s|l|f|d>).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Convert {
|
||||
I2L,
|
||||
I2F,
|
||||
I2D,
|
||||
I2B,
|
||||
I2C,
|
||||
I2S,
|
||||
L2I,
|
||||
L2F,
|
||||
L2D,
|
||||
F2I,
|
||||
F2L,
|
||||
F2D,
|
||||
D2I,
|
||||
D2L,
|
||||
D2F,
|
||||
}
|
||||
|
||||
/// Тип элемента массива для `newarray`/`*aload`/`*astore`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ArrayKind {
|
||||
Boolean,
|
||||
Char,
|
||||
Float,
|
||||
Double,
|
||||
Byte,
|
||||
Short,
|
||||
Int,
|
||||
Long,
|
||||
Reference,
|
||||
}
|
||||
|
||||
/// Тип слота локальной переменной для `*load*`/`*store*`.
|
||||
///
|
||||
/// Хранится явно: выбор опкода (`iload` vs `aload`) — подсказка верификатору,
|
||||
/// потерять её при round-trip нельзя.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SlotKind {
|
||||
Int,
|
||||
Long,
|
||||
Float,
|
||||
Double,
|
||||
Ref,
|
||||
}
|
||||
164
crates/noxis-classfile/src/insn/mod.rs
Normal file
164
crates/noxis-classfile/src/insn/mod.rs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
//! Инструкции IR: смещения отсутствуют, ветвления адресуют [`Label`].
|
||||
//!
|
||||
//! Это принципиально: любой пасс, меняющий порядок/длину инструкций, не обязан
|
||||
//! пересчитывать jump-офсеты — это делает writer один раз в конце пайплайна.
|
||||
|
||||
mod kinds;
|
||||
|
||||
pub use kinds::{
|
||||
Arith, ArrayKind, BranchCond, Cond, Const, Convert, InvokeKind, Label, Prim, SlotKind,
|
||||
};
|
||||
|
||||
/// Мнемоника одной инструкции.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Insn {
|
||||
/// Маркер метки. Ветвления указывают на [`Label`], writer вычисляет смещения.
|
||||
Mark(Label),
|
||||
Nop,
|
||||
AconstNull,
|
||||
Const(Const),
|
||||
/// Загрузка локальной переменной (`iload`, `aload`, ...).
|
||||
Load {
|
||||
slot: u16,
|
||||
kind: SlotKind,
|
||||
},
|
||||
/// Сохранение локальной переменной.
|
||||
Store {
|
||||
slot: u16,
|
||||
kind: SlotKind,
|
||||
},
|
||||
/// `iinc` — увеличение локальной переменной на константу.
|
||||
Iinc {
|
||||
index: u16,
|
||||
delta: i32,
|
||||
},
|
||||
/// Загрузка элемента массива.
|
||||
ArrayLoad(ArrayKind),
|
||||
/// Сохранение элемента массива.
|
||||
ArrayStore(ArrayKind),
|
||||
Pop,
|
||||
Pop2,
|
||||
Dup,
|
||||
DupX1,
|
||||
DupX2,
|
||||
Dup2,
|
||||
Dup2X1,
|
||||
Dup2X2,
|
||||
Swap,
|
||||
/// `iadd`, `lmul`, `lneg`, ... — тип + операция.
|
||||
Binary {
|
||||
prim: Prim,
|
||||
op: Arith,
|
||||
},
|
||||
/// Сравнение длин (`lcmp`) и плавающих (`fcmpl`/`fcmpg`).
|
||||
Cmp {
|
||||
prim: Prim,
|
||||
nan_great: bool,
|
||||
},
|
||||
/// Конверсия типов (`i2l`, `d2f`, ...).
|
||||
Convert(Convert),
|
||||
Branch(BranchCond, Label),
|
||||
Goto(Label),
|
||||
/// `jsr` — вызов подпрограммы (только классы версии ≤ 50).
|
||||
Jsr(Label),
|
||||
/// `ret` — возврат из подпрограммы (`jsr`/`ret` парны).
|
||||
Ret(u16),
|
||||
TableSwitch {
|
||||
default: Label,
|
||||
low: i32,
|
||||
high: i32,
|
||||
targets: Vec<Label>,
|
||||
},
|
||||
LookupSwitch {
|
||||
default: Label,
|
||||
pairs: Vec<(i32, Label)>,
|
||||
},
|
||||
/// `return`/`ireturn`/...; `None` — пустой возврат.
|
||||
Return(Option<Prim>),
|
||||
/// `areturn` — возврат ссылки (у [`Prim`] нет ссылочного типа).
|
||||
AReturn,
|
||||
GetStatic {
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
},
|
||||
PutStatic {
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
},
|
||||
GetField {
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
},
|
||||
PutField {
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
},
|
||||
Invoke {
|
||||
kind: InvokeKind,
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
itf: bool,
|
||||
},
|
||||
InvokeDynamic {
|
||||
name: String,
|
||||
descriptor: String,
|
||||
bootstrap: u16,
|
||||
},
|
||||
New(String),
|
||||
NewArray(ArrayKind),
|
||||
ANewArray(String),
|
||||
/// `multianewarray` — массив любого измерения; `dims` — сколько измерений со стека.
|
||||
MultiANewArray {
|
||||
descriptor: String,
|
||||
dims: u8,
|
||||
},
|
||||
ArrayLength,
|
||||
AThrow,
|
||||
CheckCast(String),
|
||||
InstanceOf(String),
|
||||
MonitorEnter,
|
||||
MonitorExit,
|
||||
}
|
||||
|
||||
impl Insn {
|
||||
/// Метки, на которые ссылается инструкция (для построения CFG).
|
||||
pub fn targets(&self) -> Vec<Label> {
|
||||
match self {
|
||||
Insn::Branch(_, label) | Insn::Goto(label) | Insn::Jsr(label) => vec![*label],
|
||||
Insn::TableSwitch {
|
||||
default,
|
||||
targets,
|
||||
low: _,
|
||||
high: _,
|
||||
} => {
|
||||
let mut all = Vec::with_capacity(targets.len() + 1);
|
||||
all.push(*default);
|
||||
all.extend(targets.iter().copied());
|
||||
all
|
||||
}
|
||||
Insn::LookupSwitch { default, pairs } => {
|
||||
let mut all = Vec::with_capacity(pairs.len() + 1);
|
||||
all.push(*default);
|
||||
all.extend(pairs.iter().map(|(k, l)| {
|
||||
let _ = k;
|
||||
*l
|
||||
}));
|
||||
all
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Незакодированный переход (для вставки opaque-предикатов и перестройки CFG).
|
||||
pub fn branch_target_mut(&mut self) -> Option<&mut Label> {
|
||||
match self {
|
||||
Insn::Branch(_, label) | Insn::Goto(label) => Some(label),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
29
crates/noxis-classfile/src/lib.rs
Normal file
29
crates/noxis-classfile/src/lib.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! # noxis-classfile
|
||||
//!
|
||||
//! Собственный IR для классовых файлов JVM: разбор, представление, запись.
|
||||
//!
|
||||
//! Почему свой IR, а не работа напрямую с крейтом-парсером: пассам нужна
|
||||
//! стабильная модель (метки вместо смещений, осознанный набор атрибутов),
|
||||
//! а не API третьей стороны, который меняется между версиями.
|
||||
//!
|
||||
//! Статус: модель и билдер готовы, бинарный parse/write — этап P1
|
||||
//! (см. `docs/stack.md`).
|
||||
|
||||
pub mod error;
|
||||
pub mod format;
|
||||
pub mod insn;
|
||||
pub mod model;
|
||||
|
||||
pub use error::ClassfileError;
|
||||
pub use format::{
|
||||
Hierarchy, StackMapFrame, VerificationType, compute_frames, parse, passthrough_needed, write,
|
||||
};
|
||||
pub use insn::{
|
||||
Arith, ArrayKind, BranchCond, Cond, Const, Convert, Insn, InvokeKind, Label, Prim, SlotKind,
|
||||
};
|
||||
pub use model::builder::{ClassBuilder, empty_code};
|
||||
pub use model::{
|
||||
Attribute, BootstrapMethod, ClassFile, Code, ExceptionHandler, Field, InnerClass, LineNumber,
|
||||
LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource,
|
||||
Version,
|
||||
};
|
||||
152
crates/noxis-classfile/src/model/builder.rs
Normal file
152
crates/noxis-classfile/src/model/builder.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! Сборка классов «с нуля»: junk-классы, рантайм-декодеры, аннотации-шум.
|
||||
|
||||
use crate::model::{Attribute, ClassFile, Code, Field, Method, Version};
|
||||
|
||||
/// Строитель класса без бинарного представления на входе.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClassBuilder {
|
||||
version: Version,
|
||||
access_flags: u16,
|
||||
this_class: String,
|
||||
super_class: Option<String>,
|
||||
interfaces: Vec<String>,
|
||||
fields: Vec<Field>,
|
||||
methods: Vec<Method>,
|
||||
attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl ClassBuilder {
|
||||
/// Новый класс `public` с указанным суперклассом (`None` = `java/lang/Object`,
|
||||
/// кроме самого `java/lang/Object` — у него суперкласса нет).
|
||||
pub fn new(this_class: impl Into<String>, super_class: Option<&str>) -> Self {
|
||||
let this_class = this_class.into();
|
||||
let super_class = match super_class {
|
||||
Some(name) => Some(name.to_string()),
|
||||
None if this_class == "java/lang/Object" => None,
|
||||
None => Some("java/lang/Object".to_string()),
|
||||
};
|
||||
Self {
|
||||
version: Version::JAVA_21,
|
||||
access_flags: 0x0021, // ACC_PUBLIC | ACC_SUPER
|
||||
this_class,
|
||||
super_class,
|
||||
interfaces: Vec::new(),
|
||||
fields: Vec::new(),
|
||||
methods: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Задать версию классового файла.
|
||||
pub fn version(mut self, version: Version) -> Self {
|
||||
self.version = version;
|
||||
self
|
||||
}
|
||||
|
||||
/// Задать флаги доступа (перезаписывает `ACC_PUBLIC|ACC_SUPER`).
|
||||
pub fn access_flags(mut self, flags: u16) -> Self {
|
||||
self.access_flags = flags;
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить интерфейс.
|
||||
pub fn interface(mut self, name: impl Into<String>) -> Self {
|
||||
self.interfaces.push(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить поле.
|
||||
pub fn field(mut self, name: impl Into<String>, descriptor: impl Into<String>) -> Self {
|
||||
self.fields.push(Field {
|
||||
access_flags: 0x0001, // ACC_PUBLIC
|
||||
name: name.into(),
|
||||
descriptor: descriptor.into(),
|
||||
attributes: Vec::new(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить метод без тела (`abstract`/`native` должны выставить флаги сами).
|
||||
pub fn method(
|
||||
mut self,
|
||||
access_flags: u16,
|
||||
name: impl Into<String>,
|
||||
descriptor: impl Into<String>,
|
||||
) -> Self {
|
||||
self.methods.push(Method {
|
||||
access_flags,
|
||||
name: name.into(),
|
||||
descriptor: descriptor.into(),
|
||||
code: None,
|
||||
attributes: Vec::new(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить метод с телом.
|
||||
pub fn method_with_code(
|
||||
mut self,
|
||||
access_flags: u16,
|
||||
name: impl Into<String>,
|
||||
descriptor: impl Into<String>,
|
||||
code: Code,
|
||||
) -> Self {
|
||||
self.methods.push(Method {
|
||||
access_flags,
|
||||
name: name.into(),
|
||||
descriptor: descriptor.into(),
|
||||
code: Some(code),
|
||||
attributes: Vec::new(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Добавить атрибут уровня класса.
|
||||
pub fn attribute(mut self, attribute: Attribute) -> Self {
|
||||
self.attributes.push(attribute);
|
||||
self
|
||||
}
|
||||
|
||||
/// Собрать класс.
|
||||
pub fn build(self) -> ClassFile {
|
||||
ClassFile {
|
||||
version: self.version,
|
||||
access_flags: self.access_flags,
|
||||
this_class: self.this_class,
|
||||
super_class: self.super_class,
|
||||
interfaces: self.interfaces,
|
||||
fields: self.fields,
|
||||
methods: self.methods,
|
||||
attributes: self.attributes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Пустое тело метода (`return`), для junk-методов.
|
||||
pub fn empty_code(max_locals: u16) -> Code {
|
||||
Code {
|
||||
max_stack: 0,
|
||||
max_locals,
|
||||
insns: vec![crate::insn::Insn::Return(None)],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builds_class_with_method() {
|
||||
let class = ClassBuilder::new("pkg/A", None)
|
||||
.field("f", "I")
|
||||
.method_with_code(0x0009, "m", "()V", empty_code(0))
|
||||
.build();
|
||||
assert_eq!(class.this_class, "pkg/A");
|
||||
assert_eq!(class.fields.len(), 1);
|
||||
assert!(class.method("m", "()V").unwrap().has_code());
|
||||
}
|
||||
}
|
||||
236
crates/noxis-classfile/src/model/mod.rs
Normal file
236
crates/noxis-classfile/src/model/mod.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
//! Структура «программы» — единица работы пайплайна.
|
||||
|
||||
pub mod builder;
|
||||
pub mod version;
|
||||
|
||||
pub use version::Version;
|
||||
|
||||
use crate::format::StackMapFrame;
|
||||
use crate::insn::{Const, Insn, Label};
|
||||
|
||||
/// Неизвестные/специфичные атрибуты хранятся байтами — round-trip без потерь.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Attribute {
|
||||
SourceFile(String),
|
||||
LineNumberTable(Vec<LineNumber>),
|
||||
LocalVariableTable(Vec<LocalVariable>),
|
||||
Signature(String),
|
||||
Exceptions(Vec<String>),
|
||||
Deprecated,
|
||||
Synthetic,
|
||||
InnerClasses(Vec<InnerClass>),
|
||||
/// `ConstantValue` поля (`static final`): значение из пула.
|
||||
ConstantValue(Const),
|
||||
/// `BootstrapMethods` для `invokedynamic` (лямбды): метод + аргументы.
|
||||
BootstrapMethods(Vec<BootstrapMethod>),
|
||||
/// `EnclosingMethod`: класс и опциональный метод, содержащие локальный класс.
|
||||
EnclosingMethod {
|
||||
class: String,
|
||||
method: Option<(String, String)>,
|
||||
},
|
||||
/// `MethodParameters`: имена формальных параметров (`None` — безымянный).
|
||||
MethodParameters(Vec<MethodParameter>),
|
||||
/// `Record`: компоненты record-класса.
|
||||
Record(Vec<RecordComponent>),
|
||||
/// `LocalVariableTypeTable`: сигнатуры generic-локальных (как LVT).
|
||||
LocalVariableTypeTable(Vec<LocalVariableType>),
|
||||
/// Всё остальное: имя + сырые байты `info` (осторожно с индексами пула).
|
||||
Raw {
|
||||
name: String,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LineNumber {
|
||||
/// Метка начала (байты вычисляет writer — переживает вставки пассов).
|
||||
pub start: Label,
|
||||
pub line: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalVariable {
|
||||
/// Метки диапазона видимости (writer пересчитает в байты).
|
||||
pub start: Label,
|
||||
pub end: Label,
|
||||
pub name: String,
|
||||
pub descriptor: String,
|
||||
pub index: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct InnerClass {
|
||||
pub inner: String,
|
||||
pub outer: Option<String>,
|
||||
pub inner_name: Option<String>,
|
||||
pub access_flags: u16,
|
||||
}
|
||||
|
||||
/// Один `bootstrap`-метод: ссылка вида `MethodHandle` + аргументы-константы.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BootstrapMethod {
|
||||
/// Номер `reference_kind` (JVMS §5.4.3.5).
|
||||
pub kind: u8,
|
||||
pub owner: String,
|
||||
pub name: String,
|
||||
pub descriptor: String,
|
||||
pub itf: bool,
|
||||
pub args: Vec<Const>,
|
||||
}
|
||||
|
||||
/// Формальный параметр метода (`MethodParameters`).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MethodParameter {
|
||||
pub name: Option<String>,
|
||||
pub access_flags: u16,
|
||||
}
|
||||
|
||||
/// Компонент `Record`: поле record-класса с атрибутами.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RecordComponent {
|
||||
pub name: String,
|
||||
pub descriptor: String,
|
||||
pub attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
/// Строка `LocalVariableTypeTable` (как `LocalVariable`, но с сигнатурой).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocalVariableType {
|
||||
/// Метки диапазона видимости (writer пересчитает в байты).
|
||||
pub start: Label,
|
||||
pub end: Label,
|
||||
pub name: String,
|
||||
pub signature: String,
|
||||
pub index: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Field {
|
||||
pub access_flags: u16,
|
||||
pub name: String,
|
||||
pub descriptor: String,
|
||||
pub attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Method {
|
||||
pub access_flags: u16,
|
||||
pub name: String,
|
||||
pub descriptor: String,
|
||||
pub code: Option<Code>,
|
||||
pub attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
/// `native`/`abstract` тела не имеют.
|
||||
pub fn has_code(&self) -> bool {
|
||||
self.code.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Тело метода.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Code {
|
||||
pub max_stack: u16,
|
||||
pub max_locals: u16,
|
||||
/// Маркеры меток + инструкции; ветвления указывают на [`Label`].
|
||||
pub insns: Vec<Insn>,
|
||||
pub exception_table: Vec<ExceptionHandler>,
|
||||
pub frames: Vec<StackMapFrame>,
|
||||
pub attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl Code {
|
||||
/// Все метки, объявленные в теле метода.
|
||||
pub fn labels(&self) -> Vec<Label> {
|
||||
self.insns
|
||||
.iter()
|
||||
.filter_map(|i| match i {
|
||||
Insn::Mark(l) => Some(*l),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ExceptionHandler {
|
||||
/// `None` — catch-all (`finally`).
|
||||
pub start: Label,
|
||||
pub end: Label,
|
||||
pub handler: Label,
|
||||
/// Внутреннее имя класса или `None` для `any`.
|
||||
pub catch_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Один `.class` в нормализованном виде.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ClassFile {
|
||||
pub version: Version,
|
||||
pub access_flags: u16,
|
||||
pub this_class: String,
|
||||
pub super_class: Option<String>,
|
||||
pub interfaces: Vec<String>,
|
||||
pub fields: Vec<Field>,
|
||||
pub methods: Vec<Method>,
|
||||
pub attributes: Vec<Attribute>,
|
||||
}
|
||||
|
||||
impl ClassFile {
|
||||
/// Ищет метод по имени и дескриптору.
|
||||
pub fn method(&self, name: &str, descriptor: &str) -> Option<&Method> {
|
||||
self.methods
|
||||
.iter()
|
||||
.find(|m| m.name == name && m.descriptor == descriptor)
|
||||
}
|
||||
|
||||
/// Ищет метод по имени и дескриптору (мутирующий доступ).
|
||||
pub fn method_mut(&mut self, name: &str, descriptor: &str) -> Option<&mut Method> {
|
||||
self.methods
|
||||
.iter_mut()
|
||||
.find(|m| m.name == name && m.descriptor == descriptor)
|
||||
}
|
||||
|
||||
/// Признак `ACC_INTERFACE`.
|
||||
pub fn is_interface(&self) -> bool {
|
||||
self.access_flags & 0x0200 != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Ресурс внутри архива (JSON, properties, картинки, ...).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Resource {
|
||||
pub path: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Вся входная программа: классы + ресурсы.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Program {
|
||||
pub classes: Vec<ClassFile>,
|
||||
pub resources: Vec<Resource>,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
/// Класс по внутреннему имени (`pkg/Class`).
|
||||
pub fn class(&self, internal_name: &str) -> Option<&ClassFile> {
|
||||
self.classes.iter().find(|c| c.this_class == internal_name)
|
||||
}
|
||||
|
||||
/// Класс по внутреннему имени (мутирующий доступ).
|
||||
pub fn class_mut(&mut self, internal_name: &str) -> Option<&mut ClassFile> {
|
||||
self.classes
|
||||
.iter_mut()
|
||||
.find(|c| c.this_class == internal_name)
|
||||
}
|
||||
|
||||
/// Ресурс по пути внутри архива.
|
||||
pub fn resource(&self, path: &str) -> Option<&Resource> {
|
||||
self.resources.iter().find(|r| r.path == path)
|
||||
}
|
||||
|
||||
/// Пустая ли программа.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.classes.is_empty() && self.resources.is_empty()
|
||||
}
|
||||
}
|
||||
26
crates/noxis-classfile/src/model/version.rs
Normal file
26
crates/noxis-classfile/src/model/version.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Версия классового файла (major/minor).
|
||||
|
||||
/// Версия классового файла.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Version {
|
||||
pub minor: u16,
|
||||
pub major: u16,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
/// Java 8 (52).
|
||||
pub const JAVA_8: Version = Version {
|
||||
minor: 0,
|
||||
major: 52,
|
||||
};
|
||||
/// Java 17 (61).
|
||||
pub const JAVA_17: Version = Version {
|
||||
minor: 0,
|
||||
major: 61,
|
||||
};
|
||||
/// Java 21 (65).
|
||||
pub const JAVA_21: Version = Version {
|
||||
minor: 0,
|
||||
major: 65,
|
||||
};
|
||||
}
|
||||
BIN
crates/noxis-classfile/tests/data/GuiItemConfig.class
Normal file
BIN
crates/noxis-classfile/tests/data/GuiItemConfig.class
Normal file
Binary file not shown.
BIN
crates/noxis-classfile/tests/data/MessageUtil.class
Normal file
BIN
crates/noxis-classfile/tests/data/MessageUtil.class
Normal file
Binary file not shown.
BIN
crates/noxis-classfile/tests/data/PaginationUtil.class
Normal file
BIN
crates/noxis-classfile/tests/data/PaginationUtil.class
Normal file
Binary file not shown.
BIN
crates/noxis-classfile/tests/data/TimeUtil.class
Normal file
BIN
crates/noxis-classfile/tests/data/TimeUtil.class
Normal file
Binary file not shown.
134
crates/noxis-classfile/tests/frames.rs
Normal file
134
crates/noxis-classfile/tests/frames.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
//! Приёмка `compute_frames` на фикстурах (`tests/data`).
|
||||
//!
|
||||
//! Кадры сносятся и считаются заново. Судьи: детерминизм + round-trip IR +
|
||||
//! настоящий `java -Xverify:all` для классов без внешних зависимостей.
|
||||
//! (`verify_class` backend'а для приёмки не годится: он ждёт кадры в байтах,
|
||||
//! а `from_bytes` отдаёт в индексах — см. `todo.md`.)
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use noxis_classfile::{Hierarchy, compute_frames, format};
|
||||
|
||||
/// Карта `класс → суперкласс` для LUB.
|
||||
#[derive(Default)]
|
||||
struct MapHierarchy {
|
||||
supers: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Hierarchy for MapHierarchy {
|
||||
fn superclass(&self, class: &str) -> Option<String> {
|
||||
self.supers.get(class).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Классы только на JDK — их `java` проверит полностью.
|
||||
const JDK_ONLY: [&str; 2] = [
|
||||
"dev/loki/lorep/util/TimeUtil",
|
||||
"dev/loki/lorep/util/PaginationUtil",
|
||||
];
|
||||
|
||||
/// Фикстуры: снести кадры, посчитать заново, проверить выход.
|
||||
#[test]
|
||||
fn fixtures_recompute_and_verify() {
|
||||
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data");
|
||||
let mut names: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.expect("tests/data")
|
||||
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
|
||||
.filter(|path| path.extension().is_some_and(|ext| ext == "class"))
|
||||
.collect();
|
||||
names.sort();
|
||||
assert!(!names.is_empty(), "нет фикстур в tests/data");
|
||||
|
||||
let mut supers = BTreeMap::new();
|
||||
let mut parsed = Vec::new();
|
||||
for name in &names {
|
||||
let class = format::parse(&std::fs::read(name).expect("read")).expect("parse");
|
||||
supers.insert(class.this_class.clone(), class.super_class.clone());
|
||||
parsed.push(class);
|
||||
}
|
||||
// Без суперкласса — только `java/lang/Object`.
|
||||
supers.entry("java/lang/Object".to_string()).or_insert(None);
|
||||
let hierarchy = MapHierarchy {
|
||||
supers: supers
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| v.map(|sup| (k, sup)))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let scratch = std::env::temp_dir().join(format!("noxis-frames-{}", std::process::id()));
|
||||
let java = java_available();
|
||||
for class in &mut parsed {
|
||||
if class.version.major < 50 {
|
||||
continue;
|
||||
}
|
||||
for method in &mut class.methods {
|
||||
if method.code.is_none() {
|
||||
continue;
|
||||
}
|
||||
let first = compute_frames(&class.this_class, method, &hierarchy).expect("recompute");
|
||||
method.code.as_mut().expect("code").frames = first.clone();
|
||||
// Детерминизм: повторный прогон даёт те же кадры.
|
||||
let second = compute_frames(&class.this_class, method, &hierarchy).expect("recompute");
|
||||
assert_eq!(first, second, "недетерминированные кадры");
|
||||
}
|
||||
// Round-trip: записанное читается в тот же IR (включая кадры).
|
||||
let written = format::write(class).expect("write");
|
||||
assert_eq!(
|
||||
format::parse(&written).expect("re-parse"),
|
||||
*class,
|
||||
"round-trip"
|
||||
);
|
||||
if java && JDK_ONLY.contains(&class.this_class.as_str()) {
|
||||
java_verify(&scratch, class, &written).expect("java -Xverify:all");
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Есть ли `java` для проверки настоящим верификатором.
|
||||
fn java_available() -> bool {
|
||||
Command::new("java")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.is_ok_and(|out| out.status.success())
|
||||
}
|
||||
|
||||
/// Записать класс в дерево пакетов и прогнать `java -Xverify:all`.
|
||||
/// Успех — загрузка без ошибок верификации (`main` при этом отсутствует).
|
||||
fn java_verify(
|
||||
scratch: &Path,
|
||||
class: &noxis_classfile::ClassFile,
|
||||
bytes: &[u8],
|
||||
) -> Result<(), String> {
|
||||
let mut path = scratch.to_path_buf();
|
||||
for part in class.this_class.split('/') {
|
||||
path.push(part);
|
||||
}
|
||||
path.set_extension("class");
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).map_err(|e| format!("mkdir: {e}"))?;
|
||||
std::fs::write(&path, bytes).map_err(|e| format!("write: {e}"))?;
|
||||
let dotted = class.this_class.replace('/', ".");
|
||||
let out = Command::new("java")
|
||||
.arg("-Xverify:all")
|
||||
.arg("-cp")
|
||||
.arg(scratch)
|
||||
.arg(&dotted)
|
||||
.output()
|
||||
.map_err(|e| format!("запуск java: {e}"))?;
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
for bad in [
|
||||
"VerifyError",
|
||||
"ClassFormatError",
|
||||
"NoClassDefFoundError",
|
||||
"LinkageError",
|
||||
] {
|
||||
if stderr.contains(bad) {
|
||||
return Err(format!("{dotted}:\n{stderr}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
221
crates/noxis-classfile/tests/roundtrip.rs
Normal file
221
crates/noxis-classfile/tests/roundtrip.rs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
//! Round-trip `parse → write → parse` через backend.
|
||||
//!
|
||||
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
|
||||
//! а не байты. Записанные байты обязаны проходить структурный `verify`
|
||||
//! backend'а — это ловит битые ссылки пула и кривые атрибуты.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use ristretto_classfile::attributes::{
|
||||
Attribute as RtAttr, BootstrapMethod as RtBsm, ExceptionTableEntry as RtEntry,
|
||||
Instruction as Rt, LineNumber as RtLine, StackFrame as RtFrame,
|
||||
};
|
||||
use ristretto_classfile::{
|
||||
ClassAccessFlags, ClassFile as RtClass, ConstantPool, Field as RtField, FieldAccessFlags,
|
||||
FieldType, JAVA_21, Method as RtMethod, MethodAccessFlags, ReferenceKind,
|
||||
};
|
||||
use noxis_classfile::{ClassFile, format};
|
||||
|
||||
/// Минимальный класс: версия, флаги, поле с `ConstantValue`, `SourceFile`.
|
||||
#[test]
|
||||
fn minimal_class_round_trip() {
|
||||
let mut pool = ConstantPool::default();
|
||||
let this = pool.add_class("demo/Round").expect("add_class");
|
||||
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||
let name = pool.add_utf8("K").expect("name");
|
||||
let desc = pool.add_utf8("I").expect("desc");
|
||||
let value = pool.add_integer(42).expect("value");
|
||||
let source_name = pool.add_utf8("SourceFile").expect("attr");
|
||||
let source_file = pool.add_utf8("Round.java").expect("file");
|
||||
let constant_name = pool.add_utf8("ConstantValue").expect("attr");
|
||||
|
||||
let rt = RtClass {
|
||||
version: JAVA_21,
|
||||
constant_pool: pool,
|
||||
access_flags: ClassAccessFlags::PUBLIC,
|
||||
this_class: this,
|
||||
super_class: object,
|
||||
interfaces: vec![],
|
||||
fields: vec![RtField {
|
||||
access_flags: FieldAccessFlags::PUBLIC
|
||||
| FieldAccessFlags::STATIC
|
||||
| FieldAccessFlags::FINAL,
|
||||
name_index: name,
|
||||
descriptor_index: desc,
|
||||
field_type: FieldType::parse("I").expect("field type"),
|
||||
attributes: vec![RtAttr::ConstantValue {
|
||||
name_index: constant_name,
|
||||
constant_value_index: value,
|
||||
}],
|
||||
}],
|
||||
methods: vec![],
|
||||
attributes: vec![RtAttr::SourceFile {
|
||||
name_index: source_name,
|
||||
source_file_index: source_file,
|
||||
}],
|
||||
..RtClass::default()
|
||||
};
|
||||
|
||||
let ir = round_trip(&rt);
|
||||
assert_eq!(ir.this_class, "demo/Round");
|
||||
assert_eq!(ir.super_class.as_deref(), Some("java/lang/Object"));
|
||||
assert_eq!(ir.methods.len(), 0);
|
||||
assert_eq!(ir.fields.len(), 1);
|
||||
}
|
||||
|
||||
/// Метод с ветвлениями, handler'ом, номерами строк и кадрами.
|
||||
#[test]
|
||||
fn code_with_branches_round_trip() {
|
||||
let mut pool = ConstantPool::default();
|
||||
let this = pool.add_class("demo/Flow").expect("add_class");
|
||||
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||
let name = pool.add_utf8("run").expect("name");
|
||||
let desc = pool.add_utf8("()I").expect("desc");
|
||||
let code_name = pool.add_utf8("Code").expect("attr");
|
||||
let lines_name = pool.add_utf8("LineNumberTable").expect("attr");
|
||||
let frames_name = pool.add_utf8("StackMapTable").expect("attr");
|
||||
|
||||
// Логические номера: 0 iconst_0, 1 istore_1, 2 iload_1, 3 ifeq→6,
|
||||
// 4 iconst_1, 5 goto→7, 6 iconst_2, 7 ireturn.
|
||||
let code = vec![
|
||||
Rt::Iconst_0,
|
||||
Rt::Istore_1,
|
||||
Rt::Iload_1,
|
||||
Rt::Ifeq(6),
|
||||
Rt::Iconst_1,
|
||||
Rt::Goto(7),
|
||||
Rt::Iconst_2,
|
||||
Rt::Ireturn,
|
||||
];
|
||||
let code_attr = RtAttr::Code {
|
||||
name_index: code_name,
|
||||
max_stack: 1,
|
||||
max_locals: 2,
|
||||
code,
|
||||
exception_table: vec![RtEntry {
|
||||
range_pc: 0..4,
|
||||
handler_pc: 6,
|
||||
catch_type: 0,
|
||||
}],
|
||||
attributes: vec![
|
||||
RtAttr::LineNumberTable {
|
||||
name_index: lines_name,
|
||||
line_numbers: vec![
|
||||
RtLine {
|
||||
start_pc: 0,
|
||||
line_number: 10,
|
||||
},
|
||||
RtLine {
|
||||
start_pc: 6,
|
||||
line_number: 20,
|
||||
},
|
||||
],
|
||||
},
|
||||
RtAttr::StackMapTable {
|
||||
name_index: frames_name,
|
||||
frames: vec![
|
||||
RtFrame::SameFrame { frame_type: 6 },
|
||||
RtFrame::SameFrame { frame_type: 0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
let rt = RtClass {
|
||||
version: JAVA_21,
|
||||
constant_pool: pool,
|
||||
access_flags: ClassAccessFlags::PUBLIC,
|
||||
this_class: this,
|
||||
super_class: object,
|
||||
interfaces: vec![],
|
||||
fields: vec![],
|
||||
methods: vec![RtMethod {
|
||||
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
|
||||
name_index: name,
|
||||
descriptor_index: desc,
|
||||
attributes: vec![code_attr],
|
||||
}],
|
||||
attributes: vec![],
|
||||
..RtClass::default()
|
||||
};
|
||||
|
||||
let ir = round_trip(&rt);
|
||||
let method = &ir.methods[0];
|
||||
let code = method.code.as_ref().expect("есть Code");
|
||||
assert_eq!(code.max_stack, 1);
|
||||
assert_eq!(code.max_locals, 2);
|
||||
// Номера инструкций: 0,1,2,3(ifeq),4,5(goto),6,7; кадры на 6 и 7.
|
||||
let frames = &code.frames;
|
||||
assert_eq!(frames.len(), 2);
|
||||
assert!(format!("{frames:?}").contains("offset_delta: 6"));
|
||||
assert_eq!(code.exception_table.len(), 1);
|
||||
assert_eq!(code.attributes.len(), 1);
|
||||
}
|
||||
|
||||
/// `BootstrapMethods` + `invokedynamic` переживают round-trip.
|
||||
#[test]
|
||||
fn bootstrap_methods_round_trip() {
|
||||
let mut pool = ConstantPool::default();
|
||||
let this = pool.add_class("demo/Lambda").expect("add_class");
|
||||
let object = pool.add_class("java/lang/Object").expect("add_object");
|
||||
let owner = pool.add_class("demo/RT").expect("owner");
|
||||
let method_ref = pool.add_method_ref(owner, "boot", "()V").expect("ref");
|
||||
let handle = pool
|
||||
.add_method_handle(ReferenceKind::InvokeStatic, method_ref)
|
||||
.expect("handle");
|
||||
let arg = pool.add_string("x").expect("arg");
|
||||
let bsm_name = pool.add_utf8("BootstrapMethods").expect("attr");
|
||||
let name = pool.add_utf8("run").expect("name");
|
||||
let desc = pool.add_utf8("()V").expect("desc");
|
||||
let code_name = pool.add_utf8("Code").expect("attr");
|
||||
let dynamic = pool.add_invoke_dynamic(0, "run", "()V").expect("indy");
|
||||
|
||||
let rt = RtClass {
|
||||
version: JAVA_21,
|
||||
constant_pool: pool,
|
||||
access_flags: ClassAccessFlags::PUBLIC,
|
||||
this_class: this,
|
||||
super_class: object,
|
||||
interfaces: vec![],
|
||||
fields: vec![],
|
||||
methods: vec![RtMethod {
|
||||
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
|
||||
name_index: name,
|
||||
descriptor_index: desc,
|
||||
attributes: vec![RtAttr::Code {
|
||||
name_index: code_name,
|
||||
max_stack: 1,
|
||||
max_locals: 0,
|
||||
code: vec![Rt::Invokedynamic(dynamic), Rt::Return],
|
||||
exception_table: vec![],
|
||||
attributes: vec![],
|
||||
}],
|
||||
}],
|
||||
attributes: vec![RtAttr::BootstrapMethods {
|
||||
name_index: bsm_name,
|
||||
methods: vec![RtBsm {
|
||||
bootstrap_method_ref: handle,
|
||||
arguments: vec![arg],
|
||||
}],
|
||||
}],
|
||||
..RtClass::default()
|
||||
};
|
||||
|
||||
let ir = round_trip(&rt);
|
||||
assert_eq!(ir.attributes.len(), 1);
|
||||
assert!(format!("{:?}", ir.attributes[0]).contains("BootstrapMethods"));
|
||||
}
|
||||
|
||||
/// `parse → write → parse` даёт тот же IR; записанное верифицируется.
|
||||
fn round_trip(rt: &RtClass<'_>) -> ClassFile {
|
||||
let mut original = Vec::new();
|
||||
rt.to_bytes(&mut original).expect("to_bytes");
|
||||
let first = format::parse(&original).expect("parse");
|
||||
let written = format::write(&first).expect("write");
|
||||
RtClass::from_bytes(&written)
|
||||
.expect("written parses")
|
||||
.verify()
|
||||
.expect("written verifies");
|
||||
let second = format::parse(&written).expect("re-parse");
|
||||
assert_eq!(first, second, "IR обязан пережить round-trip");
|
||||
second
|
||||
}
|
||||
152
crates/noxis-classfile/tests/spike_frames.rs
Normal file
152
crates/noxis-classfile/tests/spike_frames.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
|
||||
//!
|
||||
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
|
||||
//! сам и отдаёт ли их наружу. Если да — наша работа сводится к записи; если
|
||||
//! нет — кадры придётся считать (или вендорить их `verifiers::bytecode`).
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use ristretto_classfile::attributes::{Attribute, Instruction, StackFrame};
|
||||
use ristretto_classfile::verifiers::bytecode::{
|
||||
FallbackStrategy, VerificationPath, VerifierConfig, verify_class,
|
||||
};
|
||||
use ristretto_classfile::verifiers::context::VerificationContext;
|
||||
use ristretto_classfile::verifiers::error::Result as VerifyResult;
|
||||
use ristretto_classfile::{
|
||||
ClassAccessFlags, ClassFile, Constant, ConstantPool, MethodAccessFlags, Version,
|
||||
};
|
||||
|
||||
/// Разрешающий контекст: в spike'е нас интересует только сам вывод типов.
|
||||
#[derive(Debug)]
|
||||
struct Permissive;
|
||||
|
||||
impl VerificationContext for Permissive {
|
||||
fn is_subclass(&self, _subclass: &str, _superclass: &str) -> VerifyResult<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn is_assignable(&self, _target: &str, _source: &str) -> VerifyResult<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn common_superclass(&self, _class1: &str, _class2: &str) -> VerifyResult<String> {
|
||||
Ok("java/lang/Object".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// `void run()` с одной ветвью: на сдвиге 3 (`Ifeq`-target) кадр обязателен.
|
||||
fn branch_class() -> ClassFile<'static> {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let utf8_name = constant_pool
|
||||
.add(Constant::utf8("demo/Branch"))
|
||||
.expect("utf8");
|
||||
let this_class = constant_pool
|
||||
.add(Constant::Class(utf8_name))
|
||||
.expect("class");
|
||||
let run = constant_pool.add(Constant::utf8("run")).expect("utf8");
|
||||
let descriptor = constant_pool.add(Constant::utf8("()V")).expect("utf8");
|
||||
let code = constant_pool.add(Constant::utf8("Code")).expect("utf8");
|
||||
|
||||
ClassFile {
|
||||
version: Version::Java8 { minor: 0 },
|
||||
constant_pool,
|
||||
access_flags: ClassAccessFlags::PUBLIC,
|
||||
this_class,
|
||||
super_class: 0,
|
||||
interfaces: Vec::new(),
|
||||
fields: Vec::new(),
|
||||
methods: vec![ristretto_classfile::Method {
|
||||
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
|
||||
name_index: run,
|
||||
descriptor_index: descriptor,
|
||||
attributes: vec![Attribute::Code {
|
||||
name_index: code,
|
||||
max_stack: 1,
|
||||
max_locals: 0,
|
||||
code: vec![
|
||||
Instruction::Iconst_1,
|
||||
Instruction::Ifeq(3),
|
||||
Instruction::Return,
|
||||
Instruction::Return,
|
||||
],
|
||||
exception_table: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
}],
|
||||
}],
|
||||
attributes: Vec::new(),
|
||||
code_source_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn inference_config() -> VerifierConfig {
|
||||
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
|
||||
}
|
||||
|
||||
/// Без `StackMapTable` inference обязан пройти сам — значит, движок кадров у
|
||||
/// крейта есть, просто его результат не отдаётся наружу.
|
||||
#[test]
|
||||
fn inference_path_verifies_without_stack_map_table() {
|
||||
let class_file = branch_class();
|
||||
let results = verify_class(&class_file, &Permissive, &inference_config()).expect("verify");
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].success, "ошибка: {:?}", results[0].error);
|
||||
assert_eq!(results[0].path_used, VerificationPath::Inference);
|
||||
}
|
||||
|
||||
/// Строгий режим без кадров обязан упасть — иначе требование `StackMapTable`
|
||||
/// для классов ≥ 50 не проверяется вовсе.
|
||||
#[test]
|
||||
fn strict_mode_rejects_missing_stack_map_table() {
|
||||
let class_file = branch_class();
|
||||
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
||||
let outcome = verify_class(&class_file, &Permissive, &config);
|
||||
|
||||
// Строгий режим падает либо `Err`, либо `success == false` — оба варианта
|
||||
// приемлемы, важен сам отказ.
|
||||
let rejected = match &outcome {
|
||||
Err(_) => true,
|
||||
Ok(results) => results.iter().any(|r| !r.success),
|
||||
};
|
||||
assert!(rejected, "строгий режим обязан отвергнуть класс без кадров");
|
||||
}
|
||||
|
||||
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
|
||||
///
|
||||
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` — тогда
|
||||
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
|
||||
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
|
||||
/// кадр до нас не доходит: из крейта достаётся только место ошибки
|
||||
/// («merge point at offset N»), но не её содержимое.
|
||||
///
|
||||
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать — нет.
|
||||
#[test]
|
||||
fn diagnostic_exposes_expected_frame_on_failure() {
|
||||
let mut class_file = branch_class();
|
||||
let name_index = class_file
|
||||
.constant_pool
|
||||
.add_utf8("StackMapTable")
|
||||
.expect("utf8");
|
||||
// Кадр на сдвиге 0, а настоящий якорь — на сдвиге 3.
|
||||
class_file.attributes.push(Attribute::StackMapTable {
|
||||
name_index,
|
||||
frames: vec![StackFrame::SameFrame { frame_type: 0 }],
|
||||
});
|
||||
// Кладём атрибут в метод, а не в класс: `StackMapTable` живёт в `Code`.
|
||||
let stack_map = class_file.attributes.pop().expect("frame");
|
||||
match &mut class_file.methods[0].attributes[0] {
|
||||
Attribute::Code { attributes, .. } => attributes.push(stack_map),
|
||||
other => panic!("ожидался Code, получено {other:?}"),
|
||||
}
|
||||
|
||||
let strict = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
||||
let outcome = verify_class(&class_file, &Permissive, &strict);
|
||||
|
||||
// Наблюдаемое поведение: `Err`, а не `Ok` с `diagnostic`.
|
||||
let err = outcome.expect_err("битые кадры обязаны быть отвергнуты");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
message.contains("StackMapTable"),
|
||||
"ожидалась ошибка про кадры, получено: {message}"
|
||||
);
|
||||
}
|
||||
110
crates/noxis-classfile/tests/spike_ristretto.rs
Normal file
110
crates/noxis-classfile/tests/spike_ristretto.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//! Spike: пригоден ли `ristretto_classfile` как backend для чтения/записи.
|
||||
//!
|
||||
//! Критерии — `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
|
||||
//! spike'а не устаревали: если крейт сломает хотя бы один пункт, сборка упадёт,
|
||||
//! и решение о backend'е придётся пересмотреть.
|
||||
//!
|
||||
//! Что здесь **не** проверяется (и что выносится в задачу P1): конвертация
|
||||
//! нашего IR в их типы и обратно. Spike отвечает только на вопрос «можно ли
|
||||
//! на этом построить», а не «сколько на это уйдёт времени».
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use ristretto_classfile::attributes::{StackFrame, VerificationType};
|
||||
use ristretto_classfile::{ClassFile, ConstantPool, JAVA_21};
|
||||
|
||||
/// Крейт компилируется на заявленном MSRV и собирает валидный класс.
|
||||
#[test]
|
||||
fn builds_verifies_and_serializes() {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let this_class = constant_pool.add_class("demo/Hello").expect("add_class");
|
||||
|
||||
let class_file = ClassFile {
|
||||
version: JAVA_21,
|
||||
constant_pool,
|
||||
this_class,
|
||||
..ClassFile::default()
|
||||
};
|
||||
class_file.verify().expect("verify");
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
class_file.to_bytes(&mut bytes).expect("to_bytes");
|
||||
assert!(
|
||||
bytes.starts_with(&[0xCA, 0xFE, 0xBA, 0xBE]),
|
||||
"нет магии CAFEBABE"
|
||||
);
|
||||
}
|
||||
|
||||
/// Round-trip: `to_bytes` → `from_bytes` сохраняет содержимое класса.
|
||||
#[test]
|
||||
fn round_trip_preserves_identity() {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let this_class = constant_pool.add_class("demo/Round").expect("add_class");
|
||||
let source = ClassFile {
|
||||
version: JAVA_21,
|
||||
constant_pool,
|
||||
this_class,
|
||||
..ClassFile::default()
|
||||
};
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
source.to_bytes(&mut bytes).expect("to_bytes");
|
||||
let restored = ClassFile::from_bytes(&bytes).expect("from_bytes");
|
||||
|
||||
assert_eq!(
|
||||
restored.class_name().expect("class_name").to_string(),
|
||||
"demo/Round"
|
||||
);
|
||||
assert_eq!(restored.version, source.version);
|
||||
assert_eq!(restored.constant_pool.len(), source.constant_pool.len());
|
||||
}
|
||||
|
||||
/// Кадры `StackMapTable` конструируются и сериализуются публичным API —
|
||||
/// значит, мы можем **записывать** кадры, а не только читать.
|
||||
#[test]
|
||||
fn stack_map_frames_are_constructible() {
|
||||
let mut constant_pool = ConstantPool::default();
|
||||
let this_class = constant_pool.add_class("demo/Frames").expect("add_class");
|
||||
let mut class_file = ClassFile {
|
||||
version: JAVA_21,
|
||||
constant_pool,
|
||||
this_class,
|
||||
..ClassFile::default()
|
||||
};
|
||||
|
||||
// SameFrame + расширенный кадр с одним элементом на стеке.
|
||||
let frames = vec![
|
||||
StackFrame::SameFrame { frame_type: 0 },
|
||||
StackFrame::SameLocals1StackItemFrameExtended {
|
||||
frame_type: 247,
|
||||
offset_delta: 3,
|
||||
stack: vec![VerificationType::Integer],
|
||||
},
|
||||
];
|
||||
|
||||
let name_index = class_file
|
||||
.constant_pool
|
||||
.add_utf8("StackMapTable")
|
||||
.expect("add_utf8");
|
||||
class_file
|
||||
.attributes
|
||||
.push(ristretto_classfile::attributes::Attribute::StackMapTable { name_index, frames });
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
class_file.to_bytes(&mut bytes).expect("to_bytes");
|
||||
let restored = ClassFile::from_bytes(&bytes).expect("from_bytes");
|
||||
|
||||
let written = restored
|
||||
.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
ristretto_classfile::attributes::Attribute::StackMapTable { frames, .. } => {
|
||||
Some(frames.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("атрибут StackMapTable должен пережить round-trip");
|
||||
|
||||
assert_eq!(written.len(), 2);
|
||||
assert_eq!(written[0], StackFrame::SameFrame { frame_type: 0 });
|
||||
}
|
||||
20
crates/noxis-core/Cargo.toml
Normal file
20
crates/noxis-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "noxis-core"
|
||||
description = "Конфигурация, детерминированный seed, маппинги и конвейер пассов"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
noxis-classfile = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis-core/build.rs
Normal file
3
crates/noxis-core/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
210
crates/noxis-core/src/config/mod.rs
Normal file
210
crates/noxis-core/src/config/mod.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
//! Конфигурация обфускации: профили, список пассов, keep-правила.
|
||||
|
||||
pub mod sections;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::sections::{
|
||||
ConstantsConfig, DebugConfig, FlowConfig, NoiseConfig, RenameConfig, StringsConfig,
|
||||
};
|
||||
use crate::error::CoreError;
|
||||
|
||||
/// Идентификатор пасса (он же ключ в конфиге, kebab-case).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum PassId {
|
||||
/// Переименование классов, членов, локальных переменных, пакетов.
|
||||
Rename,
|
||||
/// Шифрование строк + генерация рантайм-декодера.
|
||||
Strings,
|
||||
/// Обфускация числовых констант и настоящие opaque-предикаты.
|
||||
Constants,
|
||||
/// Обфускация контрольного потока.
|
||||
Flow,
|
||||
/// Junk-классы, junk-аннотации, ложный шум.
|
||||
Noise,
|
||||
/// Отладочная информация: strip / spoof.
|
||||
DebugInfo,
|
||||
/// Ресурсы и метаданные архива (JSON, mixins, refmap, services, AW).
|
||||
Resources,
|
||||
}
|
||||
|
||||
impl PassId {
|
||||
/// Строковое имя для отчётов и логов.
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
PassId::Rename => "rename",
|
||||
PassId::Strings => "strings",
|
||||
PassId::Constants => "constants",
|
||||
PassId::Flow => "flow",
|
||||
PassId::Noise => "noise",
|
||||
PassId::DebugInfo => "debug-info",
|
||||
PassId::Resources => "resources",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PassId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Базовый набор пассов профиля.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Profile {
|
||||
/// Только переименование + отладка. Быстро, минимальный риск сломать код.
|
||||
Light,
|
||||
/// Стандарт: имена, строки, константы, ресурсы, отладка, шум.
|
||||
#[default]
|
||||
Standard,
|
||||
/// Всё включено, включая агрессивный control-flow.
|
||||
Paranoid,
|
||||
/// Ничего не включено явно — только `enable` из конфига.
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
/// Пассы, включённые профилем по умолчанию.
|
||||
pub const fn baseline(self) -> &'static [PassId] {
|
||||
match self {
|
||||
Profile::Light => &[PassId::Rename, PassId::DebugInfo, PassId::Resources],
|
||||
Profile::Standard => &[
|
||||
PassId::Rename,
|
||||
PassId::Strings,
|
||||
PassId::Constants,
|
||||
PassId::Noise,
|
||||
PassId::DebugInfo,
|
||||
PassId::Resources,
|
||||
],
|
||||
Profile::Paranoid => &[
|
||||
PassId::Rename,
|
||||
PassId::Strings,
|
||||
PassId::Constants,
|
||||
PassId::Flow,
|
||||
PassId::Noise,
|
||||
PassId::DebugInfo,
|
||||
PassId::Resources,
|
||||
],
|
||||
Profile::Manual => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Полная конфигурация (TOML).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub profile: Profile,
|
||||
/// Мастер-сид; `None` — случайный, но обязательно попадает в отчёт.
|
||||
pub seed: Option<u64>,
|
||||
/// Принудительно включить пассы вне профиля.
|
||||
pub enable: Vec<PassId>,
|
||||
/// Выключить пассы профиля.
|
||||
pub disable: Vec<PassId>,
|
||||
/// Keep-паттерны внутренних имён (`com/example/Main`, `*Service`).
|
||||
pub keep: Vec<String>,
|
||||
/// Keep-паттерны путей ресурсов, которые не переписываются.
|
||||
pub keep_resources: Vec<String>,
|
||||
pub rename: RenameConfig,
|
||||
pub strings: StringsConfig,
|
||||
pub constants: ConstantsConfig,
|
||||
pub flow: FlowConfig,
|
||||
pub noise: NoiseConfig,
|
||||
pub debug: DebugConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
profile: Profile::Standard,
|
||||
seed: None,
|
||||
enable: Vec::new(),
|
||||
disable: Vec::new(),
|
||||
keep: Vec::new(),
|
||||
keep_resources: Vec::new(),
|
||||
rename: RenameConfig::default(),
|
||||
strings: StringsConfig::default(),
|
||||
constants: ConstantsConfig::default(),
|
||||
flow: FlowConfig::default(),
|
||||
noise: NoiseConfig::default(),
|
||||
debug: DebugConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Включён ли пасс с учётом профиля и явных override'ов.
|
||||
pub fn is_enabled(&self, id: PassId) -> bool {
|
||||
if self.disable.contains(&id) {
|
||||
return false;
|
||||
}
|
||||
if self.enable.contains(&id) {
|
||||
return true;
|
||||
}
|
||||
self.profile.baseline().contains(&id)
|
||||
}
|
||||
|
||||
/// Загрузить конфиг из TOML.
|
||||
pub fn from_toml(text: &str) -> Result<Self, CoreError> {
|
||||
toml::from_str(text).map_err(|e| CoreError::Config(e.to_string()))
|
||||
}
|
||||
|
||||
/// Нормализовать значения, которые легко задать нулем/пустым списком.
|
||||
pub fn sanitize(&mut self) {
|
||||
if self.rename.package_pool == 0 {
|
||||
self.rename.package_pool = 1;
|
||||
}
|
||||
if self.strings.min_len == 0 {
|
||||
self.strings.min_len = 1;
|
||||
}
|
||||
self.strings.schemes.retain(|s| !s.trim().is_empty());
|
||||
self.flow.schemes.retain(|s| !s.trim().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn profile_gate() {
|
||||
let mut cfg = Config::default();
|
||||
assert!(cfg.is_enabled(PassId::Rename));
|
||||
assert!(cfg.is_enabled(PassId::Strings));
|
||||
assert!(!cfg.is_enabled(PassId::Flow));
|
||||
|
||||
cfg.profile = Profile::Paranoid;
|
||||
assert!(cfg.is_enabled(PassId::Flow));
|
||||
|
||||
cfg.disable.push(PassId::Strings);
|
||||
assert!(!cfg.is_enabled(PassId::Strings));
|
||||
cfg.enable.push(PassId::Flow);
|
||||
cfg.profile = Profile::Light;
|
||||
assert!(cfg.is_enabled(PassId::Flow));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_toml() {
|
||||
let cfg = Config::from_toml(
|
||||
r#"
|
||||
profile = "paranoid"
|
||||
seed = 123
|
||||
keep = ["com/example/Main"]
|
||||
[rename]
|
||||
package_pool = 4
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.profile, Profile::Paranoid);
|
||||
assert_eq!(cfg.seed, Some(123));
|
||||
assert_eq!(cfg.rename.package_pool, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_keys() {
|
||||
assert!(Config::from_toml("totally_unknown = 1").is_err());
|
||||
}
|
||||
}
|
||||
185
crates/noxis-core/src/config/sections.rs
Normal file
185
crates/noxis-core/src/config/sections.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
//! Секции конфигурации: стили имён и настройки отдельных пассов.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Стиль генерации имён.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum NameStyle {
|
||||
/// `a`, `b`, ... `aa` — короткие латинские.
|
||||
ShortLatin,
|
||||
/// `I`, `l`, `O`, `0` — визуально одинаковые.
|
||||
IlO0,
|
||||
/// Случайные unicode-имена (суррогаты допустимы в modified UTF-8).
|
||||
Unicode,
|
||||
/// Смешанная латиница средней длины.
|
||||
#[default]
|
||||
MixedLatin,
|
||||
/// Псевдосмысленные слова из другого проекта (дезориентация).
|
||||
DecoyWords,
|
||||
}
|
||||
|
||||
/// Настройки переименования.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct RenameConfig {
|
||||
/// Переименовывать пакеты в случайные пути.
|
||||
pub spread_packages: bool,
|
||||
/// Размер пула случайных пакетов.
|
||||
pub package_pool: usize,
|
||||
/// Стиль имён классов.
|
||||
pub class_style: NameStyle,
|
||||
/// Стиль имён полей и методов.
|
||||
pub member_style: NameStyle,
|
||||
/// Стиль имён локальных переменных и параметров.
|
||||
pub local_style: NameStyle,
|
||||
/// Делать члены `public` (чтобы не ломать inheritance между пакетами).
|
||||
pub widen_access: bool,
|
||||
/// Переименовывать enum-константы.
|
||||
pub rename_enum_constants: bool,
|
||||
/// Переименовывать локальные переменные и параметры.
|
||||
pub rename_locals: bool,
|
||||
/// Переименовывать и не-private члены (агрессивно: безопасно для приложений,
|
||||
/// опасно для библиотек и кода с рефлексией).
|
||||
pub rename_public_members: bool,
|
||||
}
|
||||
|
||||
impl Default for RenameConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spread_packages: true,
|
||||
package_pool: 12,
|
||||
class_style: NameStyle::ShortLatin,
|
||||
member_style: NameStyle::MixedLatin,
|
||||
local_style: NameStyle::IlO0,
|
||||
widen_access: true,
|
||||
rename_enum_constants: true,
|
||||
rename_locals: true,
|
||||
rename_public_members: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройки шифрования строк.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct StringsConfig {
|
||||
/// Схемы шифра, между которыми перемешиваются строки.
|
||||
pub schemes: Vec<String>,
|
||||
/// Строки короче этого не шифруются.
|
||||
pub min_len: usize,
|
||||
/// Дробить длинные строки на чанки (меньше похоже на пул).
|
||||
pub split_above: usize,
|
||||
/// Не шифровать строки, совпадающие с этими паттернами.
|
||||
pub skip: Vec<String>,
|
||||
/// Вычислять ключ из KDF(class, method, index), а не класть рядом с текстом.
|
||||
pub derive_key: bool,
|
||||
}
|
||||
|
||||
impl Default for StringsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
schemes: vec!["xor-splitmix".to_string(), "xtea-ctr".to_string()],
|
||||
min_len: 2,
|
||||
split_above: 64,
|
||||
skip: Vec::new(),
|
||||
derive_key: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройки обфускации контрольного потока.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct FlowConfig {
|
||||
/// Схемы, между которыми выбирается случайно по методу.
|
||||
pub schemes: Vec<String>,
|
||||
/// Не трогать методы больше этого числа инструкций (бюджет размера).
|
||||
pub max_insns: usize,
|
||||
/// Вплетать opaque-предикаты в тела блоков.
|
||||
pub encrypt_block_bodies: bool,
|
||||
}
|
||||
|
||||
impl Default for FlowConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
schemes: vec![
|
||||
"state-spread".to_string(),
|
||||
"computed-switch".to_string(),
|
||||
"butterfly".to_string(),
|
||||
"exception-goto".to_string(),
|
||||
],
|
||||
max_insns: 4000,
|
||||
encrypt_block_bodies: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройки шифрования констант.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct ConstantsConfig {
|
||||
/// Схемы разбиения (`xor`, `add`, `shift`), выбор по RNG на константу.
|
||||
pub schemes: Vec<String>,
|
||||
/// Вставлять opaque-предикаты (не больше этого на метод).
|
||||
pub max_predicates: usize,
|
||||
/// Не трогать константы в этих классах (префиксы внутренних имён).
|
||||
pub skip: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ConstantsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
schemes: vec!["xor".to_string(), "add".to_string(), "shift".to_string()],
|
||||
max_predicates: 1,
|
||||
skip: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройки шума.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct NoiseConfig {
|
||||
/// Сколько junk-классов на каждый настоящий (0 = выключено).
|
||||
pub junk_ratio: usize,
|
||||
/// Число junk-аннотаций на класс.
|
||||
pub junk_annotations: usize,
|
||||
/// Вплетать junk-классы в граф достижимости (обязательно для осмысленности).
|
||||
pub weave_reachable: bool,
|
||||
}
|
||||
|
||||
impl Default for NoiseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
junk_ratio: 0,
|
||||
junk_annotations: 0,
|
||||
weave_reachable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройки отладочной информации.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct DebugConfig {
|
||||
/// Удалять `LineNumberTable`.
|
||||
pub strip_line_numbers: bool,
|
||||
/// Удалять `LocalVariableTable`.
|
||||
pub strip_local_variables: bool,
|
||||
/// Подменять `SourceFile` на ложное имя.
|
||||
pub spoof_source_file: bool,
|
||||
/// Удалять `Signature`/`InnerClasses`/`EnclosingMethod`.
|
||||
pub strip_signatures: bool,
|
||||
}
|
||||
|
||||
impl Default for DebugConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
strip_line_numbers: true,
|
||||
strip_local_variables: true,
|
||||
spoof_source_file: true,
|
||||
strip_signatures: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
29
crates/noxis-core/src/error.rs
Normal file
29
crates/noxis-core/src/error.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! Ошибки ядра пайплайна.
|
||||
|
||||
use crate::config::PassId;
|
||||
|
||||
/// Ошибка уровня ядра.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CoreError {
|
||||
#[error("ошибка ввода-вывода: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("некорректная конфигурация: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("ошибка классового файла: {0}")]
|
||||
Classfile(#[from] noxis_classfile::ClassfileError),
|
||||
|
||||
#[error("пасс `{pass}` не смог выполниться: {msg}")]
|
||||
Pass { pass: PassId, msg: String },
|
||||
}
|
||||
|
||||
impl CoreError {
|
||||
/// Ошибка конкретного пасса.
|
||||
pub fn pass(pass: PassId, msg: impl Into<String>) -> Self {
|
||||
Self::Pass {
|
||||
pass,
|
||||
msg: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
25
crates/noxis-core/src/lib.rs
Normal file
25
crates/noxis-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//! # noxis-core
|
||||
//!
|
||||
//! Ядро обфускатора: конфигурация, детерминированный seed, маппинги,
|
||||
//! контекст прогона и контракт пассов ([`pipeline::Pass`]).
|
||||
//!
|
||||
//! Ядро ничего не знает о бинарном формате `.class` и о ZIP — только о модели
|
||||
//! [`noxis_classfile::Program`]. Сборка конвейера из пассов живёт в
|
||||
//! `noxis-passes`, чтобы не было цикла зависимостей.
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod mapping;
|
||||
pub mod pipeline;
|
||||
pub mod rng;
|
||||
|
||||
pub use config::sections::{
|
||||
ConstantsConfig, DebugConfig, FlowConfig, NameStyle, NoiseConfig, RenameConfig, StringsConfig,
|
||||
};
|
||||
pub use config::{Config, PassId, Profile};
|
||||
pub use error::CoreError;
|
||||
pub use mapping::{Mapping, field_key, method_key};
|
||||
pub use pipeline::context::Context;
|
||||
pub use pipeline::report::Report;
|
||||
pub use pipeline::{Pass, PassReport, Pipeline};
|
||||
pub use rng::{Rng, derive_seed};
|
||||
112
crates/noxis-core/src/mapping.rs
Normal file
112
crates/noxis-core/src/mapping.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Обратная таблица маппингов: деобфускация имён и stack-trace translate.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Ключ метода: `owner/name+descriptor`.
|
||||
pub fn method_key(owner: &str, name: &str, descriptor: &str) -> String {
|
||||
format!("{owner}/{name}{descriptor}")
|
||||
}
|
||||
|
||||
/// Ключ поля: `owner/name:descriptor`.
|
||||
pub fn field_key(owner: &str, name: &str, descriptor: &str) -> String {
|
||||
format!("{owner}/{name}:{descriptor}")
|
||||
}
|
||||
|
||||
/// Таблицы «оригинал → обфусцированное».
|
||||
///
|
||||
/// Нужна по двум причинам: (1) перевод stack trace'ей обратно в читаемые имена
|
||||
/// (аналог ZKM Stack Trace Translate), (2) аудит — что именно поменялось.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct Mapping {
|
||||
/// internal name класса → новый internal name
|
||||
pub classes: BTreeMap<String, String>,
|
||||
/// `owner/name+desc` → новое имя метода (владелец остаётся прежним по ключу)
|
||||
pub methods: BTreeMap<String, String>,
|
||||
/// `owner/name:desc` → новое имя поля
|
||||
pub fields: BTreeMap<String, String>,
|
||||
/// имя локальной переменной/параметра в методе → новое (по номеру слота)
|
||||
pub locals: BTreeMap<String, String>,
|
||||
/// оригинальный путь ресурса → новый
|
||||
pub resources: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl Mapping {
|
||||
/// Новое имя класса, если он переименован.
|
||||
pub fn class(&self, from: &str) -> Option<&str> {
|
||||
self.classes.get(from).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Новое имя метода.
|
||||
pub fn method(&self, owner: &str, name: &str, descriptor: &str) -> Option<&str> {
|
||||
self.methods
|
||||
.get(&method_key(owner, name, descriptor))
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
/// Новое имя поля.
|
||||
pub fn field(&self, owner: &str, name: &str, descriptor: &str) -> Option<&str> {
|
||||
self.fields
|
||||
.get(&field_key(owner, name, descriptor))
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
/// Число переименований всех видов.
|
||||
pub fn len(&self) -> usize {
|
||||
self.classes.len() + self.methods.len() + self.fields.len() + self.locals.len()
|
||||
}
|
||||
|
||||
/// Пустая ли таблица.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Человекочитаемая таблица для файла маппингов и deobfuscator'а.
|
||||
pub fn to_text(&self) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("# classes\n");
|
||||
for (from, to) in &self.classes {
|
||||
out.push_str(&format!("{from} -> {to}\n"));
|
||||
}
|
||||
out.push_str("\n# methods\n");
|
||||
for (from, to) in &self.methods {
|
||||
out.push_str(&format!("{from} -> {to}\n"));
|
||||
}
|
||||
out.push_str("\n# fields\n");
|
||||
for (from, to) in &self.fields {
|
||||
out.push_str(&format!("{from} -> {to}\n"));
|
||||
}
|
||||
out.push_str("\n# resources\n");
|
||||
for (from, to) in &self.resources {
|
||||
out.push_str(&format!("{from} -> {to}\n"));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_lookup() {
|
||||
let mut m = Mapping::default();
|
||||
m.classes.insert("com/example/Main".into(), "a/B".into());
|
||||
m.methods
|
||||
.insert(method_key("a/B", "run", "()V"), "x".into());
|
||||
assert_eq!(m.class("com/example/Main"), Some("a/B"));
|
||||
assert_eq!(m.method("a/B", "run", "()V"), Some("x"));
|
||||
assert!(m.method("a/B", "other", "()V").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_report_contains_sections() {
|
||||
let mut m = Mapping::default();
|
||||
m.classes.insert("A".into(), "B".into());
|
||||
let text = m.to_text();
|
||||
assert!(text.contains("# classes"));
|
||||
assert!(text.contains("A -> B"));
|
||||
}
|
||||
}
|
||||
94
crates/noxis-core/src/pipeline/context.rs
Normal file
94
crates/noxis-core/src/pipeline/context.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
//! Контекст прогона: конфиг, мастер-сид, маппинги, заметки.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::mapping::Mapping;
|
||||
use crate::rng::{Rng, derive_seed};
|
||||
|
||||
/// Всё состояние, которое пассы читают и дописывают.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub config: Config,
|
||||
/// Мастер-сид прогона (всегда известен и попадает в отчёт).
|
||||
pub master_seed: u64,
|
||||
pub mapping: Mapping,
|
||||
/// Заметки пассов: почему что-то пропущено, что не удалось.
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Новый контекст. `seed` берётся из конфига или генерируется.
|
||||
pub fn new(mut config: Config) -> Self {
|
||||
config.sanitize();
|
||||
let master_seed = config.seed.unwrap_or_else(fresh_seed);
|
||||
Self {
|
||||
config,
|
||||
master_seed,
|
||||
mapping: Mapping::default(),
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Детерминированный RNG для домена (`"rename"`, `"strings/0"`, ...).
|
||||
///
|
||||
/// Доменное разделение: разные пассы не должны вытягивать одну
|
||||
/// последовательность — иначе их «случайности» статистически коррелируют.
|
||||
pub fn rng(&self, domain: &str) -> Rng {
|
||||
Rng::from_seed(derive_seed(self.master_seed, domain, 0))
|
||||
}
|
||||
|
||||
/// RNG с порядковым номером внутри домена.
|
||||
pub fn rng_nth(&self, domain: &str, index: u64) -> Rng {
|
||||
Rng::from_seed(derive_seed(self.master_seed, domain, index))
|
||||
}
|
||||
|
||||
/// Заметка в отчёт (видна в JSON-отчёте и в логе).
|
||||
pub fn note(&mut self, msg: impl Into<String>) {
|
||||
let msg = msg.into();
|
||||
tracing::debug!(target: "noxis", "{msg}");
|
||||
self.notes.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Нестабильный сид, когда пользователь его не задал.
|
||||
fn fresh_seed() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0x5EED_C0DE);
|
||||
let mut r = Rng::from_seed(nanos ^ 0xA5A5_A5A5_A5A5_A5A5);
|
||||
r.next_u64()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rng_is_reproducible_from_config_seed() {
|
||||
let cfg = Config {
|
||||
seed: Some(777),
|
||||
..Config::default()
|
||||
};
|
||||
let a = Context::new(cfg.clone());
|
||||
let b = Context::new(cfg);
|
||||
assert_eq!(a.master_seed, b.master_seed);
|
||||
let av: Vec<u64> = {
|
||||
let mut r = a.rng("rename");
|
||||
(0..8).map(|_| r.next_u64()).collect()
|
||||
};
|
||||
let bv: Vec<u64> = {
|
||||
let mut r = b.rng("rename");
|
||||
(0..8).map(|_| r.next_u64()).collect()
|
||||
};
|
||||
assert_eq!(av, bv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_is_recorded() {
|
||||
let mut ctx = Context::new(Config::default());
|
||||
ctx.note("тест");
|
||||
assert_eq!(ctx.notes.len(), 1);
|
||||
}
|
||||
}
|
||||
146
crates/noxis-core/src/pipeline/mod.rs
Normal file
146
crates/noxis-core/src/pipeline/mod.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
//! Конвейер пассов: единый порядок исполнения и отчёт.
|
||||
|
||||
pub mod context;
|
||||
pub mod report;
|
||||
|
||||
use serde::Serialize;
|
||||
use noxis_classfile::Program;
|
||||
|
||||
use self::context::Context;
|
||||
use self::report::Report;
|
||||
use crate::config::PassId;
|
||||
use crate::error::CoreError;
|
||||
|
||||
/// Результат одного пасса.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PassReport {
|
||||
pub pass: PassId,
|
||||
/// Сколько объектов трансформировано.
|
||||
pub transformed: usize,
|
||||
/// Сколько объектов сознательно пропущено.
|
||||
pub skipped: usize,
|
||||
/// Человекочитаемые заметки пасса.
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl PassReport {
|
||||
/// Пустой отчёт (пасс ещё не реализован).
|
||||
pub fn not_implemented(pass: PassId, todo: &'static str) -> Self {
|
||||
Self {
|
||||
pass,
|
||||
transformed: 0,
|
||||
skipped: 0,
|
||||
notes: vec![format!("не реализовано: {todo}")],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Единый контракт трансформации программы.
|
||||
pub trait Pass: Send + Sync {
|
||||
/// Идентификатор пасса (совпадает с ключом в конфиге).
|
||||
fn id(&self) -> PassId;
|
||||
|
||||
/// Применить пасс. Порядок пассов задаёт [`Pipeline`].
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError>;
|
||||
}
|
||||
|
||||
/// Упорядоченная цепочка пассов.
|
||||
///
|
||||
/// Порядок — не деталь: см. `docs/architecture.md` (rename → resources →
|
||||
/// strings → constants → flow → noise → debug), иначе часть техник ломает
|
||||
/// предыдущие или работает по уже мёртвым именам.
|
||||
#[derive(Default)]
|
||||
pub struct Pipeline {
|
||||
passes: Vec<Box<dyn Pass>>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
/// Пустой конвейер.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Добавить пасс в конец.
|
||||
pub fn push(mut self, pass: Box<dyn Pass>) -> Self {
|
||||
self.passes.push(pass);
|
||||
self
|
||||
}
|
||||
|
||||
/// Число пассов в цепочке.
|
||||
pub fn len(&self) -> usize {
|
||||
self.passes.len()
|
||||
}
|
||||
|
||||
/// Пуст ли конвейер.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.passes.is_empty()
|
||||
}
|
||||
|
||||
/// Прогнать всю программу через все пассы.
|
||||
pub fn run(&self, ctx: &mut Context, program: &mut Program) -> Result<Report, CoreError> {
|
||||
let mut report = Report {
|
||||
seed: ctx.master_seed,
|
||||
..Report::default()
|
||||
};
|
||||
report.bump("classes", program.classes.len());
|
||||
report.bump("resources", program.resources.len());
|
||||
|
||||
for pass in &self.passes {
|
||||
let id = pass.id();
|
||||
tracing::info!(target: "noxis", pass = %id, "пасс запущен");
|
||||
let result = pass.apply(ctx, program)?;
|
||||
tracing::info!(
|
||||
target: "noxis",
|
||||
pass = %id,
|
||||
transformed = result.transformed,
|
||||
skipped = result.skipped,
|
||||
"пасс завершён"
|
||||
);
|
||||
report.passes.push(result);
|
||||
}
|
||||
|
||||
report.notes = std::mem::take(&mut ctx.notes);
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
struct Stub(PassId);
|
||||
|
||||
impl Pass for Stub {
|
||||
fn id(&self) -> PassId {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn apply(
|
||||
&self,
|
||||
_ctx: &mut Context,
|
||||
_program: &mut Program,
|
||||
) -> Result<PassReport, CoreError> {
|
||||
Ok(PassReport {
|
||||
pass: self.0,
|
||||
transformed: 1,
|
||||
skipped: 0,
|
||||
notes: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_in_order_and_collects_reports() {
|
||||
let pipeline = Pipeline::new()
|
||||
.push(Box::new(Stub(PassId::Rename)))
|
||||
.push(Box::new(Stub(PassId::Strings)));
|
||||
let mut ctx = Context::new(crate::config::Config::default());
|
||||
let mut program = Program::default();
|
||||
let report = pipeline.run(&mut ctx, &mut program).unwrap();
|
||||
assert_eq!(report.passes.len(), 2);
|
||||
assert_eq!(report.passes[0].pass, PassId::Rename);
|
||||
assert_eq!(report.passes[1].pass, PassId::Strings);
|
||||
assert_eq!(report.counters.get("classes"), Some(&0));
|
||||
}
|
||||
}
|
||||
20
crates/noxis-core/src/pipeline/report.rs
Normal file
20
crates/noxis-core/src/pipeline/report.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Итог прогона: пассы + счётчики — сериализуется в JSON.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Итог прогона: пассы + счётчики — сериализуется в JSON.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct Report {
|
||||
pub seed: u64,
|
||||
pub passes: Vec<crate::pipeline::PassReport>,
|
||||
pub notes: Vec<String>,
|
||||
/// Произвольные счётчики (`classes`, `strings_encrypted`, ...).
|
||||
pub counters: std::collections::BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// Увеличить счётчик.
|
||||
pub fn bump(&mut self, key: &str, by: usize) {
|
||||
*self.counters.entry(key.to_string()).or_insert(0) += by;
|
||||
}
|
||||
}
|
||||
168
crates/noxis-core/src/rng.rs
Normal file
168
crates/noxis-core/src/rng.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
//! Детерминированный PRNG и вывод производных seed'ов.
|
||||
//!
|
||||
//! Свой генератор, а не `rand`: обфускация должна быть **воспроизводимой между
|
||||
//! версиями инструмента** — алгоритм замирает в формате, а не в изменении API
|
||||
//! чужого крейта. Схема: SplitMix64 для инициализации + xoshiro256++ для генерации.
|
||||
|
||||
/// Генератор xoshiro256++.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rng {
|
||||
s: [u64; 4],
|
||||
}
|
||||
|
||||
const PHI: u64 = 0x9E37_79B9_7F4A_7C15;
|
||||
|
||||
fn splitmix64(state: &mut u64) -> u64 {
|
||||
*state = state.wrapping_add(PHI);
|
||||
let mut z = *state;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
fn rotl(x: u64, k: u32) -> u64 {
|
||||
x.rotate_left(k)
|
||||
}
|
||||
|
||||
impl Rng {
|
||||
/// Генератор из мастер-сиди.
|
||||
pub fn from_seed(seed: u64) -> Self {
|
||||
let mut sm = seed;
|
||||
let s = [
|
||||
splitmix64(&mut sm),
|
||||
splitmix64(&mut sm),
|
||||
splitmix64(&mut sm),
|
||||
splitmix64(&mut sm),
|
||||
];
|
||||
Self { s }
|
||||
}
|
||||
|
||||
/// Следующее 64-битное значение.
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
let result = rotl(self.s[0].wrapping_add(self.s[3]), 23).wrapping_add(self.s[0]);
|
||||
let t = self.s[1] << 17;
|
||||
self.s[2] ^= self.s[0];
|
||||
self.s[3] ^= self.s[1];
|
||||
self.s[1] ^= self.s[2];
|
||||
self.s[0] ^= self.s[3];
|
||||
self.s[2] ^= t;
|
||||
self.s[3] = rotl(self.s[3], 45);
|
||||
result
|
||||
}
|
||||
|
||||
/// Значение из `[0, bound)` без смещения (отбраковка перекоса).
|
||||
pub fn below(&mut self, bound: u64) -> u64 {
|
||||
assert!(bound > 0, "bound должен быть больше нуля");
|
||||
// Пространство [0, 2^64) режем на куски размером bound; хвост отбрасываем.
|
||||
let limit = u64::MAX - (u64::MAX % bound);
|
||||
loop {
|
||||
let v = self.next_u64();
|
||||
if v < limit {
|
||||
return v % bound;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Случайный элемент среза.
|
||||
pub fn choose<'a, T>(&mut self, items: &'a [T]) -> Option<&'a T> {
|
||||
if items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&items[self.below(items.len() as u64) as usize])
|
||||
}
|
||||
}
|
||||
|
||||
/// Перемешать срез на месте (Fisher–Yates).
|
||||
pub fn shuffle<T>(&mut self, items: &mut [T]) {
|
||||
for i in (1..items.len()).rev() {
|
||||
let j = self.below((i + 1) as u64) as usize;
|
||||
items.swap(i, j);
|
||||
}
|
||||
}
|
||||
|
||||
/// Случайные байты.
|
||||
pub fn fill_bytes(&mut self, out: &mut [u8]) {
|
||||
for chunk in out.chunks_mut(8) {
|
||||
let v = self.next_u64().to_le_bytes();
|
||||
let n = chunk.len();
|
||||
chunk.copy_from_slice(&v[..n]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Вектор случайных байтов.
|
||||
pub fn bytes(&mut self, len: usize) -> Vec<u8> {
|
||||
let mut v = vec![0u8; len];
|
||||
self.fill_bytes(&mut v);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// FNV-1a 64 — детерминированный вывод производного сида.
|
||||
///
|
||||
/// Разделение доменов обязательно: пассы не должны делить одну и ту же
|
||||
/// последовательность (иначе переименование и шифрование «случайно»
|
||||
/// коррелируют, и это видно статистикой).
|
||||
pub fn derive_seed(master: u64, domain: &str, index: u64) -> u64 {
|
||||
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut mix = |byte: u8| {
|
||||
hash ^= u64::from(byte);
|
||||
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
|
||||
};
|
||||
for b in master.to_le_bytes() {
|
||||
mix(b);
|
||||
}
|
||||
for b in domain.as_bytes() {
|
||||
mix(*b);
|
||||
}
|
||||
for b in index.to_le_bytes() {
|
||||
mix(b);
|
||||
}
|
||||
hash
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deterministic_across_instances() {
|
||||
let mut a = Rng::from_seed(42);
|
||||
let mut b = Rng::from_seed(42);
|
||||
let av: Vec<u64> = (0..32).map(|_| a.next_u64()).collect();
|
||||
let bv: Vec<u64> = (0..32).map(|_| b.next_u64()).collect();
|
||||
assert_eq!(av, bv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seeds_diverge() {
|
||||
let mut a = Rng::from_seed(1);
|
||||
let mut b = Rng::from_seed(2);
|
||||
assert_ne!(a.next_u64(), b.next_u64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn below_respects_bound() {
|
||||
let mut r = Rng::from_seed(7);
|
||||
for _ in 0..1000 {
|
||||
assert!(r.below(10) < 10);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_seed_is_domain_separated() {
|
||||
assert_ne!(derive_seed(1, "rename", 0), derive_seed(1, "strings", 0));
|
||||
assert_ne!(derive_seed(1, "rename", 0), derive_seed(1, "rename", 1));
|
||||
assert_eq!(derive_seed(1, "rename", 3), derive_seed(1, "rename", 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shuffle_is_permutation() {
|
||||
let mut r = Rng::from_seed(9);
|
||||
let mut v: Vec<u32> = (0..50).collect();
|
||||
r.shuffle(&mut v);
|
||||
let mut sorted = v.clone();
|
||||
sorted.sort_unstable();
|
||||
assert_eq!(sorted, (0..50).collect::<Vec<u32>>());
|
||||
}
|
||||
}
|
||||
18
crates/noxis-jar/Cargo.toml
Normal file
18
crates/noxis-jar/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "noxis-jar"
|
||||
description = "Чтение и запись JAR: ZIP-архив, ресурсы, срез подписей"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
noxis-classfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis-jar/build.rs
Normal file
3
crates/noxis-jar/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
157
crates/noxis-jar/src/archive.rs
Normal file
157
crates/noxis-jar/src/archive.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! Чтение и запись ZIP-архива JAR.
|
||||
//!
|
||||
//! Единственная задача модуля — переместить записи из/в вектор [`Entry`],
|
||||
//! сохранив порядок и не потеряв дубликатов молча. Всё, что связано с
|
||||
//! содержимым (`Program`, маппинги, подпись), живёт в соседних модулях.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use thiserror::Error;
|
||||
use zip::CompressionMethod;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
/// Ошибки работы с архивом.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JarError {
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("zip: {0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("duplicate archive entry: {0}")]
|
||||
Duplicate(String),
|
||||
|
||||
#[error("class file: {0}")]
|
||||
Classfile(#[from] noxis_classfile::ClassfileError),
|
||||
}
|
||||
|
||||
/// Одна запись архива: имя + байты.
|
||||
///
|
||||
/// Порядок в `[`Entry`]` — это порядок в архиве, он сохраняется при
|
||||
/// чтении и записи. Метод сжатия не хранится: всё пишется deflate'ом,
|
||||
/// что детерминированно и одинаково для одинаковых данных.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
/// Создаёт запись по имени и байтам.
|
||||
pub fn new(name: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
data: data.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Читает все не-каталожные записи из `reader`, сохраняя порядок.
|
||||
///
|
||||
/// Повторяющееся имя — ошибка, а не тихое предупреждение: битый архив
|
||||
/// с двумя `META-INF/MANIFEST.MF` собрать дороже, чем отказать.
|
||||
pub fn read_entries<R: Read + Seek>(reader: R) -> Result<Vec<Entry>, JarError> {
|
||||
let mut archive = zip::ZipArchive::new(reader)?;
|
||||
let mut entries = Vec::with_capacity(archive.len());
|
||||
|
||||
for index in 0..archive.len() {
|
||||
let mut file = archive.by_index(index)?;
|
||||
if file.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let name = file.name().to_string();
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data)?;
|
||||
entries.push(Entry { name, data });
|
||||
}
|
||||
|
||||
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
ensure_unique(&names)?;
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Пишет записи в `writer` в переданном порядке.
|
||||
pub fn write_entries<W: Write + Seek>(writer: W, entries: &[Entry]) -> Result<(), JarError> {
|
||||
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
ensure_unique(&names)?;
|
||||
|
||||
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
|
||||
|
||||
let mut out = zip::ZipWriter::new(writer);
|
||||
for entry in entries {
|
||||
out.start_file(entry.name.as_str(), options)?;
|
||||
out.write_all(&entry.data)?;
|
||||
}
|
||||
out.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Читает архив с диска.
|
||||
pub fn read_file(path: &Path) -> Result<Vec<Entry>, JarError> {
|
||||
read_entries(File::open(path)?)
|
||||
}
|
||||
|
||||
/// Пишет архив на диск.
|
||||
pub fn write_file(path: &Path, entries: &[Entry]) -> Result<(), JarError> {
|
||||
let file = File::create(path)?;
|
||||
write_entries(file, entries)
|
||||
}
|
||||
|
||||
/// Отклоняет повторяющиеся имена записей.
|
||||
fn ensure_unique(names: &[String]) -> Result<(), JarError> {
|
||||
let mut seen: Vec<&str> = Vec::with_capacity(names.len());
|
||||
for name in names {
|
||||
if seen.contains(&name.as_str()) {
|
||||
return Err(JarError::Duplicate(name.clone()));
|
||||
}
|
||||
seen.push(name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn entries() -> Vec<Entry> {
|
||||
vec![
|
||||
Entry::new("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n".to_vec()),
|
||||
Entry::new("demo/A.class", vec![0xCA, 0xFE, 0xBA, 0xBE]),
|
||||
Entry::new("config/app.json", br#"{"k":1}"#.to_vec()),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_preserves_order_and_bytes() {
|
||||
let source = entries();
|
||||
let mut buffer = Vec::new();
|
||||
write_entries(Cursor::new(&mut buffer), &source).expect("write");
|
||||
|
||||
let restored = read_entries(Cursor::new(&buffer)).expect("read");
|
||||
assert_eq!(restored, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_names_are_rejected() {
|
||||
let dup = vec![
|
||||
Entry::new("a.txt", b"1".to_vec()),
|
||||
Entry::new("a.txt", b"2".to_vec()),
|
||||
];
|
||||
let mut buffer = Vec::new();
|
||||
let err = write_entries(Cursor::new(&mut buffer), &dup).expect_err("dup");
|
||||
assert!(matches!(err, JarError::Duplicate(name) if name == "a.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_is_an_io_error() {
|
||||
let err = read_file(Path::new("/definitely/not/here.jar")).expect_err("open");
|
||||
assert!(matches!(err, JarError::Io(_)));
|
||||
}
|
||||
}
|
||||
22
crates/noxis-jar/src/lib.rs
Normal file
22
crates/noxis-jar/src/lib.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//! # noxis-jar
|
||||
//!
|
||||
//! Чтение и запись JAR: ZIP-архив, классы, ресурсы, подписи.
|
||||
//!
|
||||
//! Слой разделён на три несвязанные задачи:
|
||||
//!
|
||||
//! * [`archive`] — чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
||||
//! * [`signatures`] — срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
||||
//! * [`loader`] — перевод архива в [`Program`](noxis_classfile::Program)
|
||||
//! и обратно (классы через [`format`](noxis_classfile::format), остальное
|
||||
//! как ресурсы).
|
||||
//!
|
||||
//! Обфускация ресурсов — пасс `resources` в `noxis-passes`; сюда она не
|
||||
//! входит, потому что ей нужен маппинг имён, а не I/O.
|
||||
|
||||
pub mod archive;
|
||||
pub mod loader;
|
||||
pub mod signatures;
|
||||
|
||||
pub use archive::{Entry, JarError, read_entries, read_file, write_entries, write_file};
|
||||
pub use loader::{load, save};
|
||||
pub use signatures::{is_signature_entry, strip_signatures};
|
||||
100
crates/noxis-jar/src/loader.rs
Normal file
100
crates/noxis-jar/src/loader.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//! Перевод архива в [`Program`] и обратно.
|
||||
//!
|
||||
//! Правило простое: `*.class` уходит в парсер, всё остальное — в ресурсы.
|
||||
//! Обратно классы сериализуются первыми, затем ресурсы: порядок внутри
|
||||
//! архива на поведение не влияет, а фиксированный порядок даёт
|
||||
//! воспроизводимую запись.
|
||||
|
||||
use noxis_classfile::Program;
|
||||
use noxis_classfile::format;
|
||||
use noxis_classfile::model::Resource;
|
||||
|
||||
use crate::archive::{Entry, JarError};
|
||||
|
||||
/// Разбирает записи архива: классы — через парсер, остальное — как ресурсы.
|
||||
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
||||
let mut program = Program::default();
|
||||
|
||||
for entry in entries {
|
||||
if is_class_entry(&entry.name) {
|
||||
program.classes.push(format::parse(&entry.data)?);
|
||||
} else {
|
||||
program.resources.push(Resource {
|
||||
path: entry.name,
|
||||
data: entry.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
/// Собирает записи архива из [`Program`]: сначала классы, потом ресурсы.
|
||||
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);
|
||||
entries.push(Entry::new(name, format::write(class)?));
|
||||
}
|
||||
for resource in &program.resources {
|
||||
entries.push(Entry::new(resource.path.clone(), resource.data.clone()));
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Классовый файл внутри архива: путь оканчивается на `.class`.
|
||||
fn is_class_entry(name: &str) -> bool {
|
||||
name.len() > ".class".len() && name.ends_with(".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);
|
||||
}
|
||||
|
||||
/// До P1 `format::parse` — заглушка; после P1 ожидается успешный разбор.
|
||||
#[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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
103
crates/noxis-jar/src/signatures.rs
Normal file
103
crates/noxis-jar/src/signatures.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
//! Срез невалидных подписей JAR.
|
||||
//!
|
||||
//! После перепаковки архива `META-INF/*.SF` и `*.RSA|DSA|EC` описывают
|
||||
//! **старые** байты: JVM либо отказывается запускать jar, либо ругается на
|
||||
//! подпись. Вырезать их обязательно — и это делается на уровне записей,
|
||||
//! потому что `MANIFEST.MF` остаётся, а его секции `Name:` тоже нужно
|
||||
//! выкинуть (ими занимается пасс `resources`).
|
||||
|
||||
use crate::archive::Entry;
|
||||
|
||||
/// Расширения файлов подписи в `META-INF/` (без учёта регистра).
|
||||
const SIGNATURE_EXTS: [&str; 4] = [".SF", ".RSA", ".DSA", ".EC"];
|
||||
|
||||
/// Префикс блочных файлов подписи старого образца (`META-INF/SIG-*`).
|
||||
const LEGACY_PREFIX: &str = "SIG-";
|
||||
|
||||
/// Путь каталога подписей.
|
||||
const META_INF: &str = "META-INF/";
|
||||
|
||||
/// Проверяет, является ли запись архива файлом подписи.
|
||||
///
|
||||
/// Совпадение — только для файлов, лежащих **непосредственно** в
|
||||
/// `META-INF/`: вложенный `META-INF/certs/x.SF` — обычный ресурс.
|
||||
///
|
||||
/// Регистр не важен (`meta-inf/x.sf` — тоже подпись), `MANIFEST.MF`
|
||||
/// никогда не является подписью.
|
||||
pub fn is_signature_entry(name: &str) -> bool {
|
||||
let Some(relative) = name.strip_prefix(META_INF) else {
|
||||
return false;
|
||||
};
|
||||
if relative.is_empty() || relative.contains('/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let upper = relative.to_ascii_uppercase();
|
||||
if upper.starts_with(LEGACY_PREFIX) {
|
||||
return true;
|
||||
}
|
||||
SIGNATURE_EXTS.iter().any(|ext| upper.ends_with(ext))
|
||||
}
|
||||
|
||||
/// Удаляет подписи, возвращая оставшиеся записи и число удалённых.
|
||||
///
|
||||
/// Порядок остальных записей не меняется.
|
||||
pub fn strip_signatures(entries: Vec<Entry>) -> (Vec<Entry>, usize) {
|
||||
let before = entries.len();
|
||||
let mut kept = entries;
|
||||
kept.retain(|entry| !is_signature_entry(&entry.name));
|
||||
let removed = before - kept.len();
|
||||
(kept, removed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn signature_files_under_meta_inf_are_detected() {
|
||||
for name in [
|
||||
"META-INF/FOO.SF",
|
||||
"META-INF/foo.rsa",
|
||||
"META-INF/FOO.DSA",
|
||||
"META-INF/FOO.EC",
|
||||
"META-INF/SIG-CUSTOM",
|
||||
] {
|
||||
assert!(is_signature_entry(name), "{name} should be a signature");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_and_resources_are_kept() {
|
||||
for name in [
|
||||
"META-INF/MANIFEST.MF",
|
||||
"META-INF/services/demo.Loader",
|
||||
"META-INF/certs/root.SF",
|
||||
"demo/App.class",
|
||||
"NOT-META-INF/x.SF",
|
||||
] {
|
||||
assert!(!is_signature_entry(name), "{name} must be kept");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_removes_only_signatures_and_counts_them() {
|
||||
let entries = vec![
|
||||
Entry::new("META-INF/MANIFEST.MF", b"m".to_vec()),
|
||||
Entry::new("META-INF/C.SF", b"s".to_vec()),
|
||||
Entry::new("demo/A.class", b"c".to_vec()),
|
||||
Entry::new("META-INF/C.RSA", b"b".to_vec()),
|
||||
];
|
||||
|
||||
let (kept, removed) = strip_signatures(entries);
|
||||
assert_eq!(removed, 2);
|
||||
assert_eq!(
|
||||
kept.iter()
|
||||
.map(|entry| entry.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["META-INF/MANIFEST.MF", "demo/A.class"]
|
||||
);
|
||||
}
|
||||
}
|
||||
21
crates/noxis-passes/Cargo.toml
Normal file
21
crates/noxis-passes/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "noxis-passes"
|
||||
description = "Трансформации: переименование, строки, константы, CFG, шум, отладка"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
noxis-analysis = { workspace = true }
|
||||
noxis-classfile = { workspace = true }
|
||||
noxis-core = { workspace = true }
|
||||
noxis-runtime = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis-passes/build.rs
Normal file
3
crates/noxis-passes/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
159
crates/noxis-passes/src/constants/forms.rs
Normal file
159
crates/noxis-passes/src/constants/forms.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
//! Схемы разбиения констант и opaque-предикаты.
|
||||
//!
|
||||
//! Целые константы вне `iconst`-диапазона заменяются тройками (`x^k`,
|
||||
//! `a+b`, сдвиг); opaque-предикат — всегда истинное условие с мёртвой
|
||||
//! ветвью из одного `nop` (валидно, не исполняется, ломает линейный
|
||||
//! дисассемблирование).
|
||||
|
||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim};
|
||||
use noxis_core::Rng;
|
||||
|
||||
/// Схема разбиения (имена из `ConstantsConfig::schemes`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum Scheme {
|
||||
Xor,
|
||||
Add,
|
||||
Shift,
|
||||
}
|
||||
|
||||
impl Scheme {
|
||||
pub(super) fn parse(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"xor" => Some(Scheme::Xor),
|
||||
"add" => Some(Scheme::Add),
|
||||
"shift" => Some(Scheme::Shift),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбить `int`-константу на вычисляющую её тройку.
|
||||
pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn> {
|
||||
let (first, second) = match scheme {
|
||||
Scheme::Xor => {
|
||||
let mut key = rng.next_u64() as i32;
|
||||
if key == 0 {
|
||||
key = 0x5BD1_E995u32 as i32;
|
||||
}
|
||||
(value ^ key, key)
|
||||
}
|
||||
Scheme::Add => {
|
||||
let part = rng.next_u64() as i32;
|
||||
(part, value.wrapping_sub(part))
|
||||
}
|
||||
Scheme::Shift => (value >> 1, value),
|
||||
};
|
||||
match scheme {
|
||||
Scheme::Xor => binary(first, second, Arith::Xor, Prim::Int),
|
||||
Scheme::Add => binary(first, second, Arith::Add, Prim::Int),
|
||||
Scheme::Shift => shift_form(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Разбить `long`-константу (те же три схемы на `l*`-опкодах).
|
||||
pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Insn> {
|
||||
let (first, second) = match scheme {
|
||||
Scheme::Xor => {
|
||||
let mut key = rng.next_u64() as i64;
|
||||
if key == 0 {
|
||||
key = 0x5BD1_E995_7A93_2AA9u64 as i64;
|
||||
}
|
||||
(value ^ key, key)
|
||||
}
|
||||
Scheme::Add => {
|
||||
let part = rng.next_u64() as i64;
|
||||
(part, value.wrapping_sub(part))
|
||||
}
|
||||
Scheme::Shift => (value >> 1, value),
|
||||
};
|
||||
match scheme {
|
||||
Scheme::Xor => binary_long(first, second, Arith::Xor),
|
||||
Scheme::Add => binary_long(first, second, Arith::Add),
|
||||
Scheme::Shift => shift_form_long(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// `(a, b, op)` как три инструкции.
|
||||
fn binary(first: i32, second: i32, op: Arith, prim: Prim) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Int(first)),
|
||||
Insn::Const(Const::Int(second)),
|
||||
Insn::Binary { prim, op },
|
||||
]
|
||||
}
|
||||
|
||||
fn binary_long(first: i64, second: i64, op: Arith) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Long(first)),
|
||||
Insn::Const(Const::Long(second)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// `v = ((v >> 1) << 1) | (v & 1)` — работает для любых, включая отрицательные.
|
||||
fn shift_form(value: i32) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Int(value >> 1)),
|
||||
Insn::Const(Const::Int(1)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Shl,
|
||||
},
|
||||
Insn::Const(Const::Int(value & 1)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Or,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// То же для `long` (величина сдвига — всегда `int`!).
|
||||
fn shift_form_long(value: i64) -> Vec<Insn> {
|
||||
vec![
|
||||
Insn::Const(Const::Long(value >> 1)),
|
||||
Insn::Const(Const::Int(1)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Shl,
|
||||
},
|
||||
Insn::Const(Const::Long(value & 1)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Or,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Opaque-предикат `(C*C + C) & 1 == 0` (истина для любого `C`):
|
||||
/// `[pred, ifne DEAD, goto MERGE, DEAD: nop, MERGE:]`, дальше — оригинал.
|
||||
///
|
||||
/// Метки выдаёт вызывающий код (свободные номера метода).
|
||||
pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||
let constant = rng.next_u64() as i32;
|
||||
vec![
|
||||
Insn::Const(Const::Int(constant)),
|
||||
Insn::Dup,
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Mul,
|
||||
},
|
||||
Insn::Const(Const::Int(constant)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Add,
|
||||
},
|
||||
Insn::Const(Const::Int(1)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::And,
|
||||
},
|
||||
Insn::Branch(BranchCond::Zero(Cond::Ne), dead),
|
||||
Insn::Goto(merge),
|
||||
Insn::Mark(dead),
|
||||
Insn::Nop,
|
||||
Insn::Mark(merge),
|
||||
]
|
||||
}
|
||||
215
crates/noxis-passes/src/constants/mod.rs
Normal file
215
crates/noxis-passes/src/constants/mod.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
//! Пасс шифрования констант: числа → арифметика, плюс opaque-предикаты.
|
||||
//!
|
||||
//! `float`/`double` не трогаются (битовая точность через целые — отдельно).
|
||||
//! Мелочь из `iconst`-диапазона (`-1..=5`, `0L/1L`) остаётся как есть:
|
||||
//! её шифрование только раздувает горячий код.
|
||||
|
||||
mod forms;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use noxis_analysis::ClassHierarchy;
|
||||
use noxis_classfile::{Const, Insn, Program, compute_frames};
|
||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||
|
||||
use forms::{Scheme, encrypt_int, encrypt_long, opaque_true};
|
||||
|
||||
/// Пасс шифрования констант.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct ConstantsPass;
|
||||
|
||||
impl Pass for ConstantsPass {
|
||||
fn id(&self) -> PassId {
|
||||
PassId::Constants
|
||||
}
|
||||
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||
let cfg = ctx.config.constants.clone();
|
||||
let mut schemes = Vec::with_capacity(cfg.schemes.len());
|
||||
for name in &cfg.schemes {
|
||||
schemes.push(
|
||||
Scheme::parse(name)
|
||||
.ok_or_else(|| pass_error(format!("неизвестная схема `{name}`")))?,
|
||||
);
|
||||
}
|
||||
if schemes.is_empty() {
|
||||
return Err(pass_error("нет схем шифрования"));
|
||||
}
|
||||
let mut rng = ctx.rng("constants");
|
||||
let mut replaced = 0usize;
|
||||
let mut predicates = 0usize;
|
||||
let mut skipped = 0usize;
|
||||
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
||||
|
||||
for (ci, class) in program.classes.iter_mut().enumerate() {
|
||||
if cfg
|
||||
.skip
|
||||
.iter()
|
||||
.any(|prefix| class.this_class.starts_with(prefix))
|
||||
{
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
for (mi, method) in class.methods.iter_mut().enumerate() {
|
||||
let Some(code) = method.code.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let mut fresh = Vec::with_capacity(code.insns.len());
|
||||
let mut touched = false;
|
||||
for insn in code.insns.drain(..) {
|
||||
match insn {
|
||||
Insn::Const(Const::Int(value)) if !is_tiny_int(value) => {
|
||||
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
|
||||
fresh.extend(encrypt_int(value, scheme, &mut rng));
|
||||
replaced += 1;
|
||||
touched = true;
|
||||
}
|
||||
Insn::Const(Const::Long(value)) if value != 0 && value != 1 => {
|
||||
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
|
||||
fresh.extend(encrypt_long(value, scheme, &mut rng));
|
||||
replaced += 1;
|
||||
touched = true;
|
||||
}
|
||||
other => fresh.push(other),
|
||||
}
|
||||
}
|
||||
code.insns = fresh;
|
||||
if touched {
|
||||
code.max_stack = code.max_stack.saturating_add(2);
|
||||
mutated.push((ci, mi));
|
||||
}
|
||||
}
|
||||
}
|
||||
insert_predicates(
|
||||
program,
|
||||
&mut mutated,
|
||||
cfg.max_predicates,
|
||||
&mut rng,
|
||||
&mut predicates,
|
||||
)?;
|
||||
let hierarchy = ClassHierarchy::build(program);
|
||||
for (ci, mi) in &mutated {
|
||||
refresh_frames(program, *ci, *mi, &hierarchy)?;
|
||||
}
|
||||
Ok(report(replaced, predicates, skipped))
|
||||
}
|
||||
}
|
||||
|
||||
/// Мелочь, которую шифровать невыгодно (уже `iconst`).
|
||||
fn is_tiny_int(value: i32) -> bool {
|
||||
(-1..=5).contains(&value)
|
||||
}
|
||||
|
||||
/// Opaque-предикаты в начало методов с кодом (не больше лимита на метод).
|
||||
fn insert_predicates(
|
||||
program: &mut Program,
|
||||
mutated: &mut Vec<(usize, usize)>,
|
||||
max_predicates: usize,
|
||||
rng: &mut noxis_core::Rng,
|
||||
predicates: &mut usize,
|
||||
) -> Result<(), CoreError> {
|
||||
if max_predicates == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let classes = program.classes.len();
|
||||
for ci in 0..classes {
|
||||
let methods = program.classes[ci].methods.len();
|
||||
for mi in 0..methods {
|
||||
let empty_or_missing = program.classes[ci].methods[mi]
|
||||
.code
|
||||
.as_ref()
|
||||
.is_none_or(|code| code.insns.is_empty());
|
||||
if empty_or_missing {
|
||||
continue;
|
||||
}
|
||||
let missing = || pass_error("метод пропал при вставке предиката");
|
||||
let mut top = program.classes[ci]
|
||||
.methods
|
||||
.get(mi)
|
||||
.and_then(|method| method.code.as_ref())
|
||||
.ok_or_else(missing)?
|
||||
.labels()
|
||||
.into_iter()
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
for _ in 0..max_predicates {
|
||||
top += 1;
|
||||
let dead = top;
|
||||
top += 1;
|
||||
let merge = top;
|
||||
let at = program.classes[ci]
|
||||
.methods
|
||||
.get(mi)
|
||||
.and_then(|method| method.code.as_ref())
|
||||
.ok_or_else(missing)?
|
||||
.insns
|
||||
.iter()
|
||||
.position(|insn| !matches!(insn, Insn::Mark(_)))
|
||||
.unwrap_or(0);
|
||||
let code = program.classes[ci]
|
||||
.methods
|
||||
.get_mut(mi)
|
||||
.and_then(|method| method.code.as_mut())
|
||||
.ok_or_else(missing)?;
|
||||
let mut block = opaque_true(dead, merge, rng);
|
||||
block.extend(code.insns.drain(at..));
|
||||
code.insns.splice(at..at, block);
|
||||
code.max_stack = code.max_stack.saturating_add(2);
|
||||
*predicates += 1;
|
||||
}
|
||||
if !mutated.contains(&(ci, mi)) {
|
||||
mutated.push((ci, mi));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Пересчитать фреймы затронутого метода.
|
||||
fn refresh_frames(
|
||||
program: &mut Program,
|
||||
ci: usize,
|
||||
mi: usize,
|
||||
hierarchy: &ClassHierarchy,
|
||||
) -> Result<(), CoreError> {
|
||||
let (name, snapshot) = {
|
||||
let class = program
|
||||
.classes
|
||||
.get(ci)
|
||||
.ok_or_else(|| pass_error("класс пропал"))?;
|
||||
(
|
||||
class.this_class.clone(),
|
||||
class
|
||||
.methods
|
||||
.get(mi)
|
||||
.cloned()
|
||||
.ok_or_else(|| pass_error("метод пропал"))?,
|
||||
)
|
||||
};
|
||||
let frames =
|
||||
compute_frames(&name, &snapshot, hierarchy).map_err(|e| pass_error(e.to_string()))?;
|
||||
program
|
||||
.classes
|
||||
.get_mut(ci)
|
||||
.and_then(|c| c.methods.get_mut(mi))
|
||||
.and_then(|m| m.code.as_mut())
|
||||
.ok_or_else(|| pass_error("метод пропал"))?
|
||||
.frames = frames;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report(replaced: usize, predicates: usize, skipped: usize) -> PassReport {
|
||||
PassReport {
|
||||
pass: PassId::Constants,
|
||||
transformed: replaced + predicates,
|
||||
skipped,
|
||||
notes: vec![format!(
|
||||
"констант разбито: {replaced}, предикатов: {predicates}"
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
fn pass_error(msg: impl Into<String>) -> CoreError {
|
||||
CoreError::pass(PassId::Constants, msg)
|
||||
}
|
||||
111
crates/noxis-passes/src/constants/tests.rs
Normal file
111
crates/noxis-passes/src/constants/tests.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Тесты пасса констант: формы вычисляют исходник, мелочь цела.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Prim, Program, Version};
|
||||
use noxis_core::{Config, Context, Pass};
|
||||
|
||||
use super::ConstantsPass;
|
||||
use super::forms::{Scheme, encrypt_int, encrypt_long};
|
||||
|
||||
/// Формы обратимы арифметически (интерпретатор троек/пятёрок).
|
||||
fn eval_int(insns: &[Insn]) -> i32 {
|
||||
match insns {
|
||||
[
|
||||
Insn::Const(Const::Int(a)),
|
||||
Insn::Const(Const::Int(b)),
|
||||
Insn::Binary { op, .. },
|
||||
] => {
|
||||
use noxis_classfile::Arith::{Add, Xor};
|
||||
match op {
|
||||
Xor => a ^ b,
|
||||
Add => a.wrapping_add(*b),
|
||||
_ => panic!("не та тройка: {insns:?}"),
|
||||
}
|
||||
}
|
||||
[
|
||||
Insn::Const(Const::Int(a)),
|
||||
Insn::Const(Const::Int(1)),
|
||||
Insn::Binary { .. },
|
||||
Insn::Const(Const::Int(b)),
|
||||
Insn::Binary { .. },
|
||||
] => (a << 1) | (b & 1),
|
||||
_ => panic!("не форма: {insns:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn int_forms_round_trip() {
|
||||
let mut rng = noxis_core::Rng::from_seed(11);
|
||||
for value in [-1000, -7, -6, 6, 7, 42, 65536, i32::MAX, i32::MIN] {
|
||||
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
|
||||
assert_eq!(eval_int(&encrypt_int(value, scheme, &mut rng)), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_forms_keep_shape() {
|
||||
let mut rng = noxis_core::Rng::from_seed(12);
|
||||
for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] {
|
||||
let insns = encrypt_long(1_000_000_000_000, scheme, &mut rng);
|
||||
assert!(matches!(
|
||||
insns.last(),
|
||||
Some(Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn sample() -> Program {
|
||||
let code = Code {
|
||||
max_stack: 1,
|
||||
max_locals: 0,
|
||||
insns: vec![
|
||||
Insn::Const(Const::Int(100)),
|
||||
Insn::Const(Const::Int(3)),
|
||||
Insn::Const(Const::Long(10)),
|
||||
Insn::Return(None),
|
||||
],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
Program {
|
||||
classes: vec![
|
||||
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(0x0009, "run", "()V", code)
|
||||
.build(),
|
||||
],
|
||||
resources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Пасс: большие константы разбиты, мелочь и структура целы.
|
||||
#[test]
|
||||
fn constants_split_and_predicated() {
|
||||
let mut program = sample();
|
||||
let mut ctx = Context::new(Config {
|
||||
seed: Some(9),
|
||||
..Config::default()
|
||||
});
|
||||
let report = ConstantsPass
|
||||
.apply(&mut ctx, &mut program)
|
||||
.expect("constants");
|
||||
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("код");
|
||||
let has = |v: i32| code.insns.contains(&Insn::Const(Const::Int(v)));
|
||||
assert!(!has(100), "исходная константа уцелела");
|
||||
assert!(has(3), "мелочь тронута");
|
||||
assert!(
|
||||
!code.insns.contains(&Insn::Const(Const::Long(10))),
|
||||
"long уцелел"
|
||||
);
|
||||
// Opaque-предикат: мёртвая ветвь из `nop`.
|
||||
assert!(code.insns.contains(&Insn::Nop), "нет предиката");
|
||||
assert!(report.transformed >= 3, "мало трансформаций: {report:?}");
|
||||
assert_eq!(report.skipped, 0);
|
||||
}
|
||||
246
crates/noxis-passes/src/debug.rs
Normal file
246
crates/noxis-passes/src/debug.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
//! Пасс отладочной информации: удаление и ложная подмена.
|
||||
//!
|
||||
//! Источник требований — `info/evo-weaknesses.md` §3: у разобранного
|
||||
//! образца оригинальные `SourceFile` у 160 классов, 10 218 `LineNumberTable`
|
||||
//! и имена локальных переменных. Это дешевле всего чинить и дороже всего
|
||||
//! отдавать.
|
||||
//!
|
||||
//! Важно: удалять недостаточно. Инструменты (JADX/CFR) верят отладке сильнее,
|
||||
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя —
|
||||
//! консистентное, но не связанное с реальным исходником.
|
||||
|
||||
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
|
||||
use noxis_core::{Context, CoreError, DebugConfig, Pass, PassId, PassReport};
|
||||
|
||||
use crate::namegen::NameGenerator;
|
||||
|
||||
/// Атрибут уровня класса/метода, который мы выкидываем как «сырой».
|
||||
const ENCLOSING_METHOD: &str = "EnclosingMethod";
|
||||
|
||||
/// Атрибут уровня метода с именами параметров (`-parameters`).
|
||||
const METHOD_PARAMETERS: &str = "MethodParameters";
|
||||
|
||||
/// Пасс отладочной информации.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct DebugInfoPass;
|
||||
|
||||
impl Pass for DebugInfoPass {
|
||||
fn id(&self) -> PassId {
|
||||
PassId::DebugInfo
|
||||
}
|
||||
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||
let cfg = ctx.config.debug.clone();
|
||||
let style = ctx.config.rename.class_style;
|
||||
let mut source_names =
|
||||
NameGenerator::new(ctx.rng("debug/source_file"), style).for_classes();
|
||||
|
||||
let mut stripped = 0usize;
|
||||
let mut spoofed = 0usize;
|
||||
|
||||
for class in &mut program.classes {
|
||||
spoofed += spoof_source_file(class, &mut source_names, &cfg);
|
||||
stripped += strip_signatures(&mut class.attributes, &cfg);
|
||||
|
||||
for method in &mut class.methods {
|
||||
stripped += strip_signatures(&mut method.attributes, &cfg);
|
||||
stripped += strip_parameters(method, &cfg);
|
||||
if let Some(code) = method.code.as_mut() {
|
||||
stripped += strip_code(code, &cfg);
|
||||
}
|
||||
}
|
||||
|
||||
for field in &mut class.fields {
|
||||
stripped += strip_signatures(&mut field.attributes, &cfg);
|
||||
}
|
||||
}
|
||||
|
||||
let mut notes = Vec::new();
|
||||
if stripped > 0 {
|
||||
notes.push(format!("удалено атрибутов: {stripped}"));
|
||||
}
|
||||
if spoofed > 0 {
|
||||
notes.push(format!("подменено SourceFile: {spoofed}"));
|
||||
}
|
||||
|
||||
Ok(PassReport {
|
||||
pass: PassId::DebugInfo,
|
||||
transformed: stripped + spoofed,
|
||||
skipped: 0,
|
||||
notes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Оставляет только атрибуты, для которых `keep` вернул `true`.
|
||||
fn retain(attrs: &mut Vec<Attribute>, keep: impl FnMut(&Attribute) -> bool) -> usize {
|
||||
let before = attrs.len();
|
||||
attrs.retain(keep);
|
||||
before - attrs.len()
|
||||
}
|
||||
|
||||
/// Тела методов: `LineNumberTable`, `LocalVariableTable`.
|
||||
fn strip_code(code: &mut Code, cfg: &DebugConfig) -> usize {
|
||||
retain(&mut code.attributes, |attr| match attr {
|
||||
Attribute::LineNumberTable(_) => !cfg.strip_line_numbers,
|
||||
Attribute::LocalVariableTable(_) | Attribute::LocalVariableTypeTable(_) => {
|
||||
!cfg.strip_local_variables
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Уровень класса/метода/поля: `Signature`, `InnerClasses`, `EnclosingMethod`.
|
||||
fn strip_signatures(attrs: &mut Vec<Attribute>, cfg: &DebugConfig) -> usize {
|
||||
if !cfg.strip_signatures {
|
||||
return 0;
|
||||
}
|
||||
retain(attrs, |attr| match attr {
|
||||
Attribute::Signature(_) | Attribute::InnerClasses(_) => false,
|
||||
Attribute::EnclosingMethod { .. } => false,
|
||||
Attribute::Raw { name, .. } => name != ENCLOSING_METHOD,
|
||||
_ => true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Имена параметров метода (`MethodParameters`).
|
||||
fn strip_parameters(method: &mut Method, cfg: &DebugConfig) -> usize {
|
||||
if !cfg.strip_local_variables {
|
||||
return 0;
|
||||
}
|
||||
retain(&mut method.attributes, |attr| match attr {
|
||||
Attribute::MethodParameters(_) => false,
|
||||
Attribute::Raw { name, .. } => name != METHOD_PARAMETERS,
|
||||
_ => true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Подменяет `SourceFile` ложным, но правдоподобным именем.
|
||||
fn spoof_source_file(class: &mut ClassFile, names: &mut NameGenerator, cfg: &DebugConfig) -> usize {
|
||||
if !cfg.spoof_source_file {
|
||||
return 0;
|
||||
}
|
||||
let mut replaced = 0;
|
||||
for attr in &mut class.attributes {
|
||||
if matches!(attr, Attribute::SourceFile(_)) {
|
||||
*attr = Attribute::SourceFile(format!("{}.java", names.next_name()));
|
||||
replaced += 1;
|
||||
}
|
||||
}
|
||||
replaced
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Insn, LineNumber, LocalVariable, Resource, Version};
|
||||
use noxis_core::{Config, Context, RenameConfig};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn sample() -> Program {
|
||||
let code = Code {
|
||||
max_stack: 0,
|
||||
max_locals: 1,
|
||||
insns: vec![Insn::Return(None)],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: vec![
|
||||
Attribute::LineNumberTable(vec![LineNumber { start: 0, line: 12 }]),
|
||||
Attribute::LocalVariableTable(vec![LocalVariable {
|
||||
start: 0,
|
||||
end: 0,
|
||||
name: "this".into(),
|
||||
descriptor: "Ldemo/App;".into(),
|
||||
index: 0,
|
||||
}]),
|
||||
],
|
||||
};
|
||||
|
||||
let mut class = ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(0x0001, "run", "()V", code)
|
||||
.attribute(Attribute::SourceFile("RealApp.java".into()))
|
||||
.attribute(Attribute::Signature("Ljava/lang/Object;".into()))
|
||||
.attribute(Attribute::InnerClasses(Vec::new()))
|
||||
.build();
|
||||
class.methods[0].attributes.push(Attribute::Raw {
|
||||
name: METHOD_PARAMETERS.into(),
|
||||
data: vec![],
|
||||
});
|
||||
|
||||
Program {
|
||||
classes: vec![class],
|
||||
resources: vec![Resource {
|
||||
path: "app.json".into(),
|
||||
data: br#"{}"#.to_vec(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn run(program: &mut Program, debug: DebugConfig) -> PassReport {
|
||||
let config = Config {
|
||||
seed: Some(7),
|
||||
debug,
|
||||
rename: RenameConfig {
|
||||
class_style: noxis_core::NameStyle::ShortLatin,
|
||||
..RenameConfig::default()
|
||||
},
|
||||
..Config::default()
|
||||
};
|
||||
let mut ctx = Context::new(config);
|
||||
DebugInfoPass.apply(&mut ctx, program).expect("debug pass")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_strips_and_spoofs() {
|
||||
let mut program = sample();
|
||||
let report = run(&mut program, DebugConfig::default());
|
||||
|
||||
let class = &program.classes[0];
|
||||
let code = class.methods[0].code.as_ref().expect("code");
|
||||
assert!(code.attributes.is_empty(), "атрибуты кода должны уйти");
|
||||
assert!(
|
||||
class.methods[0].attributes.is_empty(),
|
||||
"MethodParameters уходит"
|
||||
);
|
||||
|
||||
match class.attributes[0] {
|
||||
Attribute::SourceFile(ref name) => assert_ne!(name, "RealApp.java"),
|
||||
_ => panic!("SourceFile должен быть первым"),
|
||||
}
|
||||
assert_eq!(class.attributes.len(), 3, "Signature/InnerClasses остаются");
|
||||
assert!(report.transformed > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signatures_and_parameters_survive_when_disabled() {
|
||||
let debug = DebugConfig {
|
||||
strip_line_numbers: false,
|
||||
strip_local_variables: false,
|
||||
spoof_source_file: false,
|
||||
strip_signatures: false,
|
||||
};
|
||||
let mut program = sample();
|
||||
run(&mut program, debug);
|
||||
|
||||
let class = &program.classes[0];
|
||||
assert_eq!(class.methods[0].code.as_ref().unwrap().attributes.len(), 2);
|
||||
assert_eq!(class.methods[0].attributes.len(), 1);
|
||||
assert_eq!(class.attributes.len(), 3);
|
||||
assert_eq!(
|
||||
class.attributes[0],
|
||||
Attribute::SourceFile("RealApp.java".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_gives_the_same_spoofed_name() {
|
||||
let mut first = sample();
|
||||
let mut second = sample();
|
||||
run(&mut first, DebugConfig::default());
|
||||
run(&mut second, DebugConfig::default());
|
||||
assert_eq!(first.classes[0].attributes, second.classes[0].attributes);
|
||||
}
|
||||
}
|
||||
95
crates/noxis-passes/src/lib.rs
Normal file
95
crates/noxis-passes/src/lib.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
//! # noxis-passes
|
||||
//!
|
||||
//! Трансформации байткода. Сборка конвейера из пассов (`standard_pipeline`)
|
||||
//! живёт здесь же: ядро (`noxis-core`) знает только контракт [`Pass`].
|
||||
|
||||
pub mod constants;
|
||||
pub mod debug;
|
||||
pub mod namegen;
|
||||
pub mod noise;
|
||||
pub mod rename;
|
||||
pub mod resources;
|
||||
pub mod strings;
|
||||
|
||||
use noxis_core::Pass;
|
||||
|
||||
pub use constants::ConstantsPass;
|
||||
pub use debug::DebugInfoPass;
|
||||
pub use namegen::NameGenerator;
|
||||
pub use noise::NoisePass;
|
||||
pub use rename::RenamePass;
|
||||
pub use resources::ResourcesPass;
|
||||
pub use strings::StringsPass;
|
||||
|
||||
/// Стандартный конвейер в порядке [`docs/architecture.md`](../../docs/architecture.md):
|
||||
/// `rename → resources → strings → constants → flow → noise → debug`.
|
||||
///
|
||||
/// Нереализованные пассы просто не добавлены — порядок уже соблюдён,
|
||||
/// поэтому каждый следующий пасс вставляется на своё место, а не
|
||||
/// дописывается в конец.
|
||||
pub fn standard_pipeline() -> noxis_core::Pipeline {
|
||||
noxis_core::Pipeline::new()
|
||||
.push(Box::new(RenamePass) as Box<dyn Pass>)
|
||||
.push(Box::new(ResourcesPass) as Box<dyn Pass>)
|
||||
.push(Box::new(StringsPass) as Box<dyn Pass>)
|
||||
.push(Box::new(ConstantsPass) as Box<dyn Pass>)
|
||||
.push(Box::new(NoisePass) as Box<dyn Pass>)
|
||||
.push(Box::new(DebugInfoPass) as Box<dyn Pass>)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Code, Insn, Program, Version};
|
||||
use noxis_core::{Config, Context, PassId};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn sample() -> Program {
|
||||
let code = Code {
|
||||
max_stack: 0,
|
||||
max_locals: 1,
|
||||
insns: vec![Insn::Return(None)],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
Program {
|
||||
classes: vec![
|
||||
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(0x0001, "run", "()V", code)
|
||||
.build(),
|
||||
],
|
||||
resources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_pipeline_runs_end_to_end_in_order() {
|
||||
let mut ctx = Context::new(Config {
|
||||
seed: Some(1),
|
||||
..Config::default()
|
||||
});
|
||||
let mut program = sample();
|
||||
|
||||
let report = standard_pipeline()
|
||||
.run(&mut ctx, &mut program)
|
||||
.expect("pipeline");
|
||||
|
||||
let order: Vec<_> = report.passes.iter().map(|p| p.pass).collect();
|
||||
assert_eq!(
|
||||
order,
|
||||
vec![
|
||||
PassId::Rename,
|
||||
PassId::Resources,
|
||||
PassId::Strings,
|
||||
PassId::Constants,
|
||||
PassId::Noise,
|
||||
PassId::DebugInfo
|
||||
]
|
||||
);
|
||||
assert_eq!(report.counters.get("classes"), Some(&1));
|
||||
}
|
||||
}
|
||||
119
crates/noxis-passes/src/namegen/alphabet.rs
Normal file
119
crates/noxis-passes/src/namegen/alphabet.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//! Алфавиты, ключевые слова и запрещённые имена.
|
||||
//!
|
||||
//! Списки вынесены отдельно: к ним обращаются и генератор, и пасс
|
||||
//! переименования (резервирование keep-имён).
|
||||
|
||||
/// Жёсткие ключевые слова Java: имя класса/метода не может им совпадать.
|
||||
pub(super) const KEYWORDS: &[&str] = &[
|
||||
"abstract",
|
||||
"assert",
|
||||
"boolean",
|
||||
"break",
|
||||
"byte",
|
||||
"case",
|
||||
"catch",
|
||||
"char",
|
||||
"class",
|
||||
"const",
|
||||
"continue",
|
||||
"default",
|
||||
"do",
|
||||
"double",
|
||||
"else",
|
||||
"enum",
|
||||
"extends",
|
||||
"final",
|
||||
"finally",
|
||||
"float",
|
||||
"for",
|
||||
"goto",
|
||||
"if",
|
||||
"implements",
|
||||
"import",
|
||||
"instanceof",
|
||||
"int",
|
||||
"interface",
|
||||
"long",
|
||||
"native",
|
||||
"new",
|
||||
"package",
|
||||
"private",
|
||||
"protected",
|
||||
"public",
|
||||
"return",
|
||||
"short",
|
||||
"static",
|
||||
"strictfp",
|
||||
"super",
|
||||
"switch",
|
||||
"synchronized",
|
||||
"this",
|
||||
"throw",
|
||||
"throws",
|
||||
"transient",
|
||||
"try",
|
||||
"void",
|
||||
"volatile",
|
||||
"while",
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
"_",
|
||||
"var",
|
||||
"record",
|
||||
"sealed",
|
||||
"permits",
|
||||
"yield",
|
||||
];
|
||||
|
||||
/// Имена, которые не стоит затирать: мешают читать/логировать и ловятся
|
||||
/// инструментами до любой деобфускации.
|
||||
pub(super) const AVOID_CLASS: &[&str] = &[
|
||||
"Object",
|
||||
"String",
|
||||
"Class",
|
||||
"Record",
|
||||
"Enum",
|
||||
"Cloneable",
|
||||
"Iterable",
|
||||
"Comparable",
|
||||
"Thread",
|
||||
"Runnable",
|
||||
"Override",
|
||||
"Deprecated",
|
||||
"SuppressWarnings",
|
||||
];
|
||||
|
||||
pub(super) const AVOID_MEMBER: &[&str] = &[
|
||||
"toString",
|
||||
"equals",
|
||||
"hashCode",
|
||||
"clone",
|
||||
"wait",
|
||||
"notify",
|
||||
"notifyAll",
|
||||
"finalize",
|
||||
"main",
|
||||
"run",
|
||||
"call",
|
||||
"apply",
|
||||
];
|
||||
|
||||
pub(super) const LATIN_LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
|
||||
pub(super) const LATIN_UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/// Визуально неразличимые символы — ядро стиля `IlO0`.
|
||||
pub(super) const CONFUSABLE: &[u8] = b"IlO01";
|
||||
/// Unicode-буквы, валидные как Java-идентификаторы (Cyrillic/Greek/CJK).
|
||||
pub(super) const UNICODE_LETTERS: &[char] = &[
|
||||
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'у', 'ф',
|
||||
'х', 'ц', 'ч', 'ш', 'щ', 'э', 'ю', 'я', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'λ', 'μ', 'π',
|
||||
'ρ', 'σ', 'τ', 'φ', 'ω', '日', '本', '語', '中', '文', '国',
|
||||
];
|
||||
pub(super) const DECOY_LEFT: &[&str] = &[
|
||||
"broken", "silent", "quiet", "amber", "cobalt", "frozen", "hollow", "vivid", "mellow",
|
||||
"scarlet",
|
||||
];
|
||||
pub(super) const DECOY_RIGHT: &[&str] = &[
|
||||
"mirror", "lantern", "harbor", "ember", "shadow", "quarry", "meadow", "cipher", "anchor",
|
||||
"ripple",
|
||||
];
|
||||
215
crates/noxis-passes/src/namegen/mod.rs
Normal file
215
crates/noxis-passes/src/namegen/mod.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
//! Генерация имён: стили, анти-ключевые слова, уникальность в scope.
|
||||
|
||||
pub mod alphabet;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use noxis_core::{NameStyle, Rng};
|
||||
|
||||
use self::alphabet::{
|
||||
AVOID_CLASS, AVOID_MEMBER, CONFUSABLE, DECOY_LEFT, DECOY_RIGHT, KEYWORDS, LATIN_LOWER,
|
||||
LATIN_UPPER, UNICODE_LETTERS,
|
||||
};
|
||||
|
||||
/// Генератор имён с гарантированной уникальностью в пределах одного scope.
|
||||
pub struct NameGenerator {
|
||||
rng: Rng,
|
||||
style: NameStyle,
|
||||
used: HashSet<String>,
|
||||
/// Счётчик-страховка, если случайные варианты исчерпаны.
|
||||
fallback: u64,
|
||||
avoid_class: bool,
|
||||
}
|
||||
|
||||
impl NameGenerator {
|
||||
/// Генератор для указанного стиля.
|
||||
pub fn new(rng: Rng, style: NameStyle) -> Self {
|
||||
Self {
|
||||
rng,
|
||||
style,
|
||||
used: HashSet::new(),
|
||||
fallback: 0,
|
||||
avoid_class: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Режим «имена классов»: дополнительно избегаем имён из JDK/аннотаций.
|
||||
pub fn for_classes(mut self) -> Self {
|
||||
self.avoid_class = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Занять конкретное имя (защита от коллизий с keep-именами).
|
||||
pub fn reserve(&mut self, name: impl Into<String>) {
|
||||
self.used.insert(name.into());
|
||||
}
|
||||
|
||||
/// Следующее свободное имя.
|
||||
pub fn next_name(&mut self) -> String {
|
||||
for _ in 0..64 {
|
||||
let candidate = self.candidate();
|
||||
if self.acceptable(&candidate) {
|
||||
self.used.insert(candidate.clone());
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
// Страховка: детерминированный счётчик, гарантированно уникален.
|
||||
loop {
|
||||
self.fallback += 1;
|
||||
let candidate = format!("{}{}", self.style_prefix(), self.fallback);
|
||||
if self.acceptable(&candidate) {
|
||||
self.used.insert(candidate.clone());
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn style_prefix(&self) -> &'static str {
|
||||
match self.style {
|
||||
NameStyle::ShortLatin | NameStyle::IlO0 | NameStyle::MixedLatin => "z",
|
||||
NameStyle::Unicode => "\u{043e}",
|
||||
NameStyle::DecoyWords => "tmp",
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate(&mut self) -> String {
|
||||
match self.style {
|
||||
NameStyle::ShortLatin => {
|
||||
let len = 1 + self.rng.below(3) as usize;
|
||||
random_from(LATIN_LOWER, len, &mut self.rng)
|
||||
}
|
||||
NameStyle::IlO0 => {
|
||||
let len = 3 + self.rng.below(5) as usize;
|
||||
random_from(CONFUSABLE, len, &mut self.rng)
|
||||
}
|
||||
NameStyle::MixedLatin => {
|
||||
let len = 6 + self.rng.below(7) as usize;
|
||||
let mut out = String::with_capacity(len);
|
||||
for i in 0..len {
|
||||
let alpha = if self.rng.below(2) == 0 {
|
||||
LATIN_LOWER
|
||||
} else {
|
||||
LATIN_UPPER
|
||||
};
|
||||
let idx = self.rng.below(alpha.len() as u64) as usize;
|
||||
let ch = alpha[idx] as char;
|
||||
// Первый символ — только буква (не цифра).
|
||||
if i == 0 && !ch.is_ascii_alphabetic() {
|
||||
out.push('a');
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
NameStyle::Unicode => {
|
||||
let len = 4 + self.rng.below(6) as usize;
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
let idx = self.rng.below(UNICODE_LETTERS.len() as u64) as usize;
|
||||
UNICODE_LETTERS[idx]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
NameStyle::DecoyWords => {
|
||||
let l = DECOY_LEFT[self.rng.below(DECOY_LEFT.len() as u64) as usize];
|
||||
let r = DECOY_RIGHT[self.rng.below(DECOY_RIGHT.len() as u64) as usize];
|
||||
let sep = if self.rng.below(2) == 0 { "_" } else { "" };
|
||||
let mut name = format!("{l}{sep}{r}");
|
||||
if self.rng.below(2) == 0 {
|
||||
name = capitalize(&name);
|
||||
}
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn acceptable(&self, name: &str) -> bool {
|
||||
if name.is_empty() || self.used.contains(name) {
|
||||
return false;
|
||||
}
|
||||
if KEYWORDS.contains(&name) {
|
||||
return false;
|
||||
}
|
||||
if self.avoid_class && AVOID_CLASS.contains(&name) {
|
||||
return false;
|
||||
}
|
||||
if !self.avoid_class && AVOID_MEMBER.contains(&name) {
|
||||
return false;
|
||||
}
|
||||
let Some(first) = name.chars().next() else {
|
||||
return false;
|
||||
};
|
||||
if !first.is_alphabetic() && first != '_' && first != '$' {
|
||||
return false;
|
||||
}
|
||||
name.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
|
||||
}
|
||||
}
|
||||
|
||||
fn random_from(alphabet: &[u8], len: usize, rng: &mut Rng) -> String {
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
let idx = rng.below(alphabet.len() as u64) as usize;
|
||||
alphabet[idx] as char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn capitalize(s: &str) -> String {
|
||||
let mut chars = s.chars();
|
||||
match chars.next() {
|
||||
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::alphabet::{AVOID_MEMBER, KEYWORDS};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn names_are_unique_and_valid() {
|
||||
let mut names = NameGenerator::new(Rng::from_seed(1), NameStyle::ShortLatin);
|
||||
let mut seen = HashSet::new();
|
||||
for _ in 0..500 {
|
||||
let name = names.next_name();
|
||||
assert!(seen.insert(name.clone()), "повтор имени {name}");
|
||||
assert!(!KEYWORDS.contains(&name.as_str()));
|
||||
assert!(name.starts_with(|c: char| c.is_ascii_alphabetic()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn avoids_reserved_member_names() {
|
||||
let mut names = NameGenerator::new(Rng::from_seed(2), NameStyle::DecoyWords);
|
||||
for _ in 0..500 {
|
||||
let name = names.next_name();
|
||||
assert!(!AVOID_MEMBER.contains(&name.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_style_produces_identifier_valid_names() {
|
||||
let mut names = NameGenerator::new(Rng::from_seed(3), NameStyle::Unicode);
|
||||
for _ in 0..100 {
|
||||
let name = names.next_name();
|
||||
assert!(
|
||||
name.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_names_never_reissued() {
|
||||
let mut names = NameGenerator::new(Rng::from_seed(4), NameStyle::ShortLatin);
|
||||
names.reserve("abc");
|
||||
for _ in 0..300 {
|
||||
assert_ne!(names.next_name(), "abc");
|
||||
}
|
||||
}
|
||||
}
|
||||
189
crates/noxis-passes/src/noise/mod.rs
Normal file
189
crates/noxis-passes/src/noise/mod.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
//! Пасс шума: junk-классы и вплетание их в граф достижимости.
|
||||
//!
|
||||
//! Мёртвый код выкидывается shrink-анализом, поэтому каждый junk-класс
|
||||
//! получает ребро из живого метода (`invokestatic shadow/decoyN.run()V`).
|
||||
//! Junk-аннотации не генерируются: сырые атрибуты не несут свежие индексы пула.
|
||||
|
||||
mod weave;
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Program, Version, empty_code};
|
||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||
|
||||
use weave::weave_first_reachable;
|
||||
|
||||
/// Префикс детерминированных имён junk-классов.
|
||||
const JUNK_PREFIX: &str = "shadow/decoy";
|
||||
|
||||
/// Имя и дескриптор точки входа junk-класса.
|
||||
const JUNK_ENTRY: (&str, &str) = ("run", "()V");
|
||||
|
||||
/// Флаги `public static` для junk-метода.
|
||||
const ACC_PUBLIC_STATIC: u16 = 0x0009;
|
||||
|
||||
/// Пасс шума.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct NoisePass;
|
||||
|
||||
impl Pass for NoisePass {
|
||||
fn id(&self) -> PassId {
|
||||
PassId::Noise
|
||||
}
|
||||
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||
let ratio = ctx.config.noise.junk_ratio;
|
||||
let annotations = ctx.config.noise.junk_annotations;
|
||||
let weave = ctx.config.noise.weave_reachable;
|
||||
|
||||
if ratio == 0 && annotations == 0 {
|
||||
return Ok(PassReport {
|
||||
pass: PassId::Noise,
|
||||
transformed: 0,
|
||||
skipped: 0,
|
||||
notes: vec!["шум выключен: junk_ratio=0, junk_annotations=0".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
let version = program
|
||||
.classes
|
||||
.first()
|
||||
.map_or(Version::JAVA_21, |c| c.version);
|
||||
let real_classes = program.classes.len();
|
||||
let count = ratio.saturating_mul(real_classes);
|
||||
|
||||
let mut junk_names = Vec::with_capacity(count);
|
||||
let mut suffix = 0usize;
|
||||
for _ in 0..count {
|
||||
let name = loop {
|
||||
let candidate = format!("{JUNK_PREFIX}{suffix}");
|
||||
suffix = suffix.saturating_add(1);
|
||||
if program.class(&candidate).is_none() {
|
||||
break candidate;
|
||||
}
|
||||
};
|
||||
let junk = ClassBuilder::new(name.clone(), None)
|
||||
.version(version)
|
||||
.method_with_code(ACC_PUBLIC_STATIC, JUNK_ENTRY.0, JUNK_ENTRY.1, empty_code(0))
|
||||
.build();
|
||||
program.classes.push(junk);
|
||||
junk_names.push(name);
|
||||
}
|
||||
let added = junk_names.len();
|
||||
|
||||
let mut woven = 0usize;
|
||||
if weave && added > 0 {
|
||||
woven = weave_first_reachable(program, real_classes, &junk_names)?;
|
||||
}
|
||||
|
||||
let mut skipped = 0usize;
|
||||
let mut notes = Vec::new();
|
||||
if added > 0 {
|
||||
notes.push(format!("добавлено junk-классов: {added}"));
|
||||
}
|
||||
if woven > 0 {
|
||||
notes.push(format!("вплетено вызовов: {woven}"));
|
||||
}
|
||||
if added == 0 && woven == 0 {
|
||||
notes.push("нечего шуметь: нет классов или методов с кодом".to_string());
|
||||
}
|
||||
if annotations > 0 {
|
||||
skipped = annotations.saturating_mul(program.classes.len());
|
||||
notes.push(format!(
|
||||
"junk_annotations пропущен: сырые атрибуты не могут нести свежие индексы пула (пропущено: {skipped})"
|
||||
));
|
||||
}
|
||||
|
||||
for note in ¬es {
|
||||
ctx.note(note.clone());
|
||||
}
|
||||
|
||||
Ok(PassReport {
|
||||
pass: PassId::Noise,
|
||||
transformed: added + woven,
|
||||
skipped,
|
||||
notes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{Insn, InvokeKind, Resource};
|
||||
use noxis_core::{Config, NoiseConfig};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn sample() -> Program {
|
||||
Program {
|
||||
classes: vec![
|
||||
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(ACC_PUBLIC_STATIC, "run", "()V", empty_code(0))
|
||||
.build(),
|
||||
],
|
||||
resources: vec![Resource {
|
||||
path: "app.json".into(),
|
||||
data: br#"{}"#.to_vec(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn run(program: &mut Program, noise: NoiseConfig) -> PassReport {
|
||||
let config = Config {
|
||||
seed: Some(42),
|
||||
noise,
|
||||
..Config::default()
|
||||
};
|
||||
let mut ctx = Context::new(config);
|
||||
NoisePass.apply(&mut ctx, program).expect("noise pass")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adds_junk_and_weaves_call() {
|
||||
let mut program = sample();
|
||||
let noise = NoiseConfig {
|
||||
junk_ratio: 2,
|
||||
junk_annotations: 0,
|
||||
weave_reachable: true,
|
||||
};
|
||||
let report = run(&mut program, noise);
|
||||
|
||||
assert_eq!(program.classes.len(), 3);
|
||||
assert_eq!(program.classes[1].this_class, "shadow/decoy0");
|
||||
assert_eq!(program.classes[2].this_class, "shadow/decoy1");
|
||||
|
||||
let insns = &program.classes[0].methods[0].code.as_ref().unwrap().insns;
|
||||
assert_eq!(insns.len(), 2);
|
||||
match &insns[0] {
|
||||
Insn::Invoke {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
itf,
|
||||
} => {
|
||||
assert_eq!(*kind, InvokeKind::Static);
|
||||
assert_eq!(owner, "shadow/decoy0");
|
||||
assert_eq!(name, "run");
|
||||
assert_eq!(descriptor, "()V");
|
||||
assert!(!itf);
|
||||
}
|
||||
other => panic!("ожидался Invoke, получено: {other:?}"),
|
||||
}
|
||||
assert!(matches!(insns[1], Insn::Return(None)));
|
||||
assert_eq!(report.transformed, 3);
|
||||
assert_eq!(report.skipped, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_ratio_changes_nothing() {
|
||||
let mut program = sample();
|
||||
let before = program.clone();
|
||||
let report = run(&mut program, NoiseConfig::default());
|
||||
|
||||
assert_eq!(program, before);
|
||||
assert_eq!(report.transformed, 0);
|
||||
assert_eq!(report.skipped, 0);
|
||||
}
|
||||
}
|
||||
80
crates/noxis-passes/src/noise/weave.rs
Normal file
80
crates/noxis-passes/src/noise/weave.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
//! Вплетание junk-классов в граф достижимости живых методов.
|
||||
|
||||
use noxis_analysis::ClassHierarchy;
|
||||
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
|
||||
use noxis_core::{CoreError, PassId};
|
||||
|
||||
use super::JUNK_ENTRY;
|
||||
|
||||
/// Вплетает вызов первого junk-класса в первый метод с кодом.
|
||||
///
|
||||
/// Вызов встаёт перед терминалом (`Return`/`AReturn`/`AThrow`), чтобы не
|
||||
/// породить недостижимый хвост без фреймов; фреймы пересчитываются.
|
||||
pub(super) fn weave_first_reachable(
|
||||
program: &mut Program,
|
||||
real_classes: usize,
|
||||
junk_names: &[String],
|
||||
) -> Result<usize, CoreError> {
|
||||
let missing = || CoreError::pass(PassId::Noise, "цель вплетания пропала");
|
||||
let Some(owner) = junk_names.first() else {
|
||||
return Err(missing());
|
||||
};
|
||||
let target = program.classes[..real_classes.min(program.classes.len())]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(ci, c)| {
|
||||
c.methods
|
||||
.iter()
|
||||
.position(|m| m.code.is_some())
|
||||
.map(|mi| (ci, mi))
|
||||
});
|
||||
let Some((ci, mi)) = target else { return Ok(0) };
|
||||
|
||||
let class_name = program
|
||||
.classes
|
||||
.get(ci)
|
||||
.ok_or_else(missing)?
|
||||
.this_class
|
||||
.clone();
|
||||
let call = Insn::Invoke {
|
||||
kind: InvokeKind::Static,
|
||||
owner: owner.clone(),
|
||||
name: JUNK_ENTRY.0.to_string(),
|
||||
descriptor: JUNK_ENTRY.1.to_string(),
|
||||
itf: false,
|
||||
};
|
||||
{
|
||||
let code = program
|
||||
.classes
|
||||
.get_mut(ci)
|
||||
.and_then(|c| c.methods.get_mut(mi))
|
||||
.and_then(|m| m.code.as_mut())
|
||||
.ok_or_else(missing)?;
|
||||
if matches!(
|
||||
code.insns.last(),
|
||||
Some(Insn::Return(_) | Insn::AReturn | Insn::AThrow)
|
||||
) {
|
||||
code.insns.insert(code.insns.len() - 1, call);
|
||||
} else {
|
||||
code.insns.push(call);
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = program
|
||||
.classes
|
||||
.get(ci)
|
||||
.and_then(|c| c.methods.get(mi))
|
||||
.cloned()
|
||||
.ok_or_else(missing)?;
|
||||
let hierarchy = ClassHierarchy::build(program);
|
||||
let frames = compute_frames(&class_name, &snapshot, &hierarchy)
|
||||
.map_err(|e| CoreError::pass(PassId::Noise, e.to_string()))?;
|
||||
program
|
||||
.classes
|
||||
.get_mut(ci)
|
||||
.and_then(|c| c.methods.get_mut(mi))
|
||||
.and_then(|m| m.code.as_mut())
|
||||
.ok_or_else(missing)?
|
||||
.frames = frames;
|
||||
Ok(1)
|
||||
}
|
||||
237
crates/noxis-passes/src/rename/assign.rs
Normal file
237
crates/noxis-passes/src/rename/assign.rs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
//! Назначение новых имён: карты, семейства виртуальных методов,
|
||||
//! пул пакетов и финальное ширинирование доступа.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use noxis_analysis::{Analysis, ClassHierarchy};
|
||||
use noxis_classfile::Program;
|
||||
use noxis_core::{Config, Context, NameStyle, Rng};
|
||||
|
||||
use super::{ACC_PRIVATE, ACC_PROTECTED, ACC_PUBLIC, ACC_STATIC};
|
||||
use crate::namegen::NameGenerator;
|
||||
|
||||
/// `(owner, name, descriptor)` — как в JVM-ссылке.
|
||||
pub(super) type MemberKey = (String, String, String);
|
||||
|
||||
/// Карты переименований одного прогона: `declared` — где объявлено,
|
||||
/// `alias` — наследники, которые наследуют это имя без своего объявления.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct RenameMaps {
|
||||
pub(super) classes: BTreeMap<String, String>,
|
||||
pub(super) methods: BTreeMap<MemberKey, String>,
|
||||
pub(super) methods_alias: BTreeMap<MemberKey, String>,
|
||||
pub(super) fields: BTreeMap<MemberKey, String>,
|
||||
pub(super) fields_alias: BTreeMap<MemberKey, String>,
|
||||
}
|
||||
|
||||
impl RenameMaps {
|
||||
pub(super) fn method(&self, owner: &str, name: &str, desc: &str) -> Option<String> {
|
||||
let key = (owner.to_string(), name.to_string(), desc.to_string());
|
||||
self.methods
|
||||
.get(&key)
|
||||
.or_else(|| self.methods_alias.get(&key))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(super) fn field(&self, owner: &str, name: &str, desc: &str) -> Option<String> {
|
||||
let key = (owner.to_string(), name.to_string(), desc.to_string());
|
||||
self.fields
|
||||
.get(&key)
|
||||
.or_else(|| self.fields_alias.get(&key))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Псевдоними: наследник вызывает член, объявленный в предке.
|
||||
pub(super) fn expand(&mut self, hierarchy: &ClassHierarchy, owners: &[String]) {
|
||||
let methods: Vec<(MemberKey, String)> = self
|
||||
.methods
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
for ((owner, name, desc), new_name) in &methods {
|
||||
for child in owners {
|
||||
if child == owner || !hierarchy.is_subclass_of(child, owner) {
|
||||
continue;
|
||||
}
|
||||
let key = (child.clone(), name.clone(), desc.clone());
|
||||
if self.methods.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
self.methods_alias
|
||||
.entry(key)
|
||||
.or_insert_with(|| new_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let fields: Vec<(MemberKey, String)> = self
|
||||
.fields
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
for ((owner, name, desc), new_name) in &fields {
|
||||
for child in owners {
|
||||
if child == owner || !hierarchy.is_subclass_of(child, owner) {
|
||||
continue;
|
||||
}
|
||||
let key = (child.clone(), name.clone(), desc.clone());
|
||||
if self.fields.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
self.fields_alias
|
||||
.entry(key)
|
||||
.or_insert_with(|| new_name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Переименование виртуальных методов «семейством»: одно имя на всю иерархию,
|
||||
/// включая мостовые методы с ковариантным возвратом.
|
||||
pub(super) fn rename_virtual_families(
|
||||
program: &Program,
|
||||
analysis: &Analysis,
|
||||
maps: &mut RenameMaps,
|
||||
cfg: &Config,
|
||||
ctx: &mut Context,
|
||||
) -> usize {
|
||||
// Семейство = (имя, параметры): возврат игнорируем — так захватываем
|
||||
// ковариантные возвраты и мостовые методы (`ACC_BRIDGE`) разом.
|
||||
let mut families: BTreeMap<(String, String), Vec<MemberKey>> = BTreeMap::new();
|
||||
let mut locked_families: BTreeSet<(String, String)> = BTreeSet::new();
|
||||
|
||||
for class in &program.classes {
|
||||
let locked_class = analysis.is_locked(&class.this_class);
|
||||
for method in &class.methods {
|
||||
if method.name == "<init>" || method.name == "<clinit>" {
|
||||
continue;
|
||||
}
|
||||
if method.access_flags & (ACC_STATIC | ACC_PRIVATE) != 0 {
|
||||
continue;
|
||||
}
|
||||
let Some(params) = params_of(&method.descriptor) else {
|
||||
continue;
|
||||
};
|
||||
let family = (method.name.clone(), params);
|
||||
if locked_class || analysis.keep.keeps_member(&method.name) {
|
||||
locked_families.insert(family.clone());
|
||||
}
|
||||
families.entry(family).or_default().push((
|
||||
class.this_class.clone(),
|
||||
method.name.clone(),
|
||||
method.descriptor.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let mut renamed = 0usize;
|
||||
let mut family_index = 0u64;
|
||||
for (family_key, members) in families {
|
||||
if locked_families.contains(&family_key) {
|
||||
continue;
|
||||
}
|
||||
let mut names = NameGenerator::new(
|
||||
ctx.rng_nth("rename/virtual-families", family_index),
|
||||
cfg.rename.member_style,
|
||||
);
|
||||
family_index += 1;
|
||||
for (_, name, _) in &members {
|
||||
names.reserve(name.clone());
|
||||
}
|
||||
let new_name = names.next_name();
|
||||
for key in members {
|
||||
if maps.methods.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
maps.methods.insert(key, new_name.clone());
|
||||
renamed += 1;
|
||||
}
|
||||
}
|
||||
renamed
|
||||
}
|
||||
|
||||
/// `(<params>)<ret>` → `(<params>)`, либо `None`, если дескриптор кривой.
|
||||
fn params_of(descriptor: &str) -> Option<String> {
|
||||
let close = descriptor.find(')')?;
|
||||
if !descriptor.starts_with('(') {
|
||||
return None;
|
||||
}
|
||||
Some(descriptor[..=close].to_string())
|
||||
}
|
||||
|
||||
pub(super) fn parent_package(internal: &str) -> String {
|
||||
match internal.rfind('/') {
|
||||
Some(i) => internal[..i].to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn simple_name(internal: &str) -> String {
|
||||
match internal.rfind('/') {
|
||||
Some(i) => internal[i + 1..].to_string(),
|
||||
None => internal.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Пул случайных пакетов (2–3 сегмента); при выключенном разбросе — один пустой.
|
||||
pub(super) fn generate_packages(cfg: &Config, rng: &mut Rng) -> Vec<String> {
|
||||
if !cfg.rename.spread_packages {
|
||||
return vec![String::new()];
|
||||
}
|
||||
let style = match cfg.rename.class_style {
|
||||
NameStyle::Unicode => NameStyle::Unicode,
|
||||
NameStyle::IlO0 => NameStyle::IlO0,
|
||||
_ => NameStyle::MixedLatin,
|
||||
};
|
||||
let mut names = NameGenerator::new(rng.clone(), style).for_classes();
|
||||
let pool = cfg.rename.package_pool.max(1);
|
||||
(0..pool)
|
||||
.map(|_| {
|
||||
let segments = 2 + rng.below(2) as usize;
|
||||
let parts: Vec<String> = (0..segments).map(|_| names.next_name()).collect();
|
||||
parts.join("/")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Переписать дескриптор/сигнатуру: `Lpkg/Class;` → новое имя класса.
|
||||
pub(super) fn rewrite_descriptor(desc: &str, classes: &BTreeMap<String, String>) -> String {
|
||||
if !desc.contains('L') {
|
||||
return desc.to_string();
|
||||
}
|
||||
let mut out = String::with_capacity(desc.len());
|
||||
let mut chars = desc.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
if c != 'L' {
|
||||
out.push(c);
|
||||
continue;
|
||||
}
|
||||
let mut name = String::new();
|
||||
for c in chars.by_ref() {
|
||||
if c == ';' {
|
||||
break;
|
||||
}
|
||||
name.push(c);
|
||||
}
|
||||
out.push('L');
|
||||
out.push_str(classes.get(&name).map_or(name.as_str(), String::as_str));
|
||||
out.push(';');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Расширить доступ до `public`, чтобы переезд в другой пакет не ломал вызовы.
|
||||
pub(super) fn widen_all(program: &mut Program) {
|
||||
for class in program.classes.iter_mut() {
|
||||
class.access_flags = widen(class.access_flags);
|
||||
for field in class.fields.iter_mut() {
|
||||
field.access_flags = widen(field.access_flags);
|
||||
}
|
||||
for method in class.methods.iter_mut() {
|
||||
method.access_flags = widen(method.access_flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn widen(flags: u16) -> u16 {
|
||||
(flags & !(ACC_PRIVATE | ACC_PROTECTED)) | ACC_PUBLIC
|
||||
}
|
||||
232
crates/noxis-passes/src/rename/mod.rs
Normal file
232
crates/noxis-passes/src/rename/mod.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
//! Пасс переименования: классы, поля, методы, локальные переменные, дескрипторы.
|
||||
//!
|
||||
//! Это самая дешёвая и самая отдающая техника (см. `info/evo-weaknesses.md`:
|
||||
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
|
||||
//!
|
||||
//! Тонкости, которые здесь учитываются:
|
||||
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс — нужны
|
||||
//! псевдонимы (`alias`) для всей иерархии;
|
||||
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
|
||||
//! «семействе» метода — переименовывать их нужно разом;
|
||||
//! - `serialVersionUID`, JNI, рефлексия, сериализация — не трогаем.
|
||||
|
||||
/// Карты и стратегия назначения имён.
|
||||
mod assign;
|
||||
/// Переписывание тел методов и атрибутов.
|
||||
mod rewrite;
|
||||
/// Интеграционные тесты пасса.
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use noxis_analysis::{Analysis, ClassHierarchy, RenameSafety, method_rename_safety};
|
||||
use noxis_classfile::Program;
|
||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport, field_key, method_key};
|
||||
|
||||
use self::assign::{
|
||||
RenameMaps, generate_packages, parent_package, rename_virtual_families, simple_name, widen_all,
|
||||
};
|
||||
use self::rewrite::rewrite_class;
|
||||
use crate::namegen::NameGenerator;
|
||||
|
||||
const ACC_PUBLIC: u16 = 0x0001;
|
||||
const ACC_PRIVATE: u16 = 0x0002;
|
||||
const ACC_PROTECTED: u16 = 0x0004;
|
||||
const ACC_STATIC: u16 = 0x0008;
|
||||
const ACC_FINAL: u16 = 0x0010;
|
||||
const ACC_ENUM: u16 = 0x4000;
|
||||
|
||||
/// Пасс переименования.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct RenamePass;
|
||||
|
||||
impl Pass for RenamePass {
|
||||
fn id(&self) -> PassId {
|
||||
PassId::Rename
|
||||
}
|
||||
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||
let analysis = Analysis::run(program, &ctx.config)
|
||||
.map_err(|e| CoreError::pass(PassId::Rename, e.to_string()))?;
|
||||
let hierarchy = ClassHierarchy::build(program);
|
||||
let owners: Vec<String> = program
|
||||
.classes
|
||||
.iter()
|
||||
.map(|c| c.this_class.clone())
|
||||
.collect();
|
||||
let cfg = ctx.config.clone();
|
||||
|
||||
let mut maps = RenameMaps::default();
|
||||
let mut renamed_classes = 0usize;
|
||||
|
||||
// 1. Имена классов.
|
||||
{
|
||||
let packages = generate_packages(&cfg, &mut ctx.rng("rename/packages"));
|
||||
let mut names =
|
||||
NameGenerator::new(ctx.rng("rename/classes"), cfg.rename.class_style).for_classes();
|
||||
for name in &owners {
|
||||
if analysis.is_locked(name) {
|
||||
names.reserve(simple_name(name));
|
||||
}
|
||||
}
|
||||
for name in &owners {
|
||||
if analysis.is_locked(name) {
|
||||
continue;
|
||||
}
|
||||
let package = if cfg.rename.spread_packages {
|
||||
let idx = ctx
|
||||
.rng_nth("rename/pick", renamed_classes as u64)
|
||||
.below(packages.len() as u64) as usize;
|
||||
packages[idx].clone()
|
||||
} else {
|
||||
parent_package(name)
|
||||
};
|
||||
let simple = names.next_name();
|
||||
let new_name = if package.is_empty() {
|
||||
simple
|
||||
} else {
|
||||
format!("{package}/{simple}")
|
||||
};
|
||||
maps.classes.insert(name.clone(), new_name);
|
||||
renamed_classes += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Простые (не-виртуальные) члены: private/static/final.
|
||||
let mut renamed_methods = 0usize;
|
||||
let mut renamed_fields = 0usize;
|
||||
|
||||
for (idx, class) in program.classes.iter().enumerate() {
|
||||
let owner = class.this_class.clone();
|
||||
let locked = analysis.is_locked(&owner);
|
||||
let mut method_names = NameGenerator::new(
|
||||
ctx.rng_nth("rename/methods", idx as u64),
|
||||
cfg.rename.member_style,
|
||||
);
|
||||
let mut field_names = NameGenerator::new(
|
||||
ctx.rng_nth("rename/fields", idx as u64),
|
||||
cfg.rename.member_style,
|
||||
);
|
||||
|
||||
if !locked {
|
||||
for method in &class.methods {
|
||||
if method.name == "<init>" || method.name == "<clinit>" {
|
||||
method_names.reserve(method.name.clone());
|
||||
continue;
|
||||
}
|
||||
if analysis.keep.keeps_member(&method.name) {
|
||||
method_names.reserve(method.name.clone());
|
||||
continue;
|
||||
}
|
||||
let safety = method_rename_safety(
|
||||
program,
|
||||
&hierarchy,
|
||||
&owner,
|
||||
&method.name,
|
||||
&method.descriptor,
|
||||
false,
|
||||
);
|
||||
match safety {
|
||||
RenameSafety::Safe => {
|
||||
let new = method_names.next_name();
|
||||
maps.methods.insert(
|
||||
(
|
||||
owner.clone(),
|
||||
method.name.clone(),
|
||||
method.descriptor.clone(),
|
||||
),
|
||||
new,
|
||||
);
|
||||
renamed_methods += 1;
|
||||
}
|
||||
// Виртуальный не-final: переименовывается только вместе
|
||||
// со всем семейством (см. rename_virtual_families).
|
||||
_ => method_names.reserve(method.name.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
for field in &class.fields {
|
||||
if analysis.keep.keeps_member(&field.name)
|
||||
|| field.name == "serialVersionUID"
|
||||
|| field.name == "serialPersistentFields"
|
||||
{
|
||||
field_names.reserve(field.name.clone());
|
||||
continue;
|
||||
}
|
||||
let is_enum_const = field.access_flags & ACC_ENUM != 0;
|
||||
if is_enum_const && !cfg.rename.rename_enum_constants {
|
||||
field_names.reserve(field.name.clone());
|
||||
continue;
|
||||
}
|
||||
let visible = field.access_flags & (ACC_PRIVATE | ACC_STATIC | ACC_FINAL) == 0;
|
||||
if visible && !cfg.rename.rename_public_members {
|
||||
field_names.reserve(field.name.clone());
|
||||
continue;
|
||||
}
|
||||
let new = field_names.next_name();
|
||||
maps.fields.insert(
|
||||
(owner.clone(), field.name.clone(), field.descriptor.clone()),
|
||||
new,
|
||||
);
|
||||
renamed_fields += 1;
|
||||
}
|
||||
} else {
|
||||
for method in &class.methods {
|
||||
method_names.reserve(method.name.clone());
|
||||
}
|
||||
for field in &class.fields {
|
||||
field_names.reserve(field.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Виртуальные семейства — только в агрессивном режиме.
|
||||
if cfg.rename.rename_public_members {
|
||||
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
|
||||
}
|
||||
|
||||
// 4. Псевдоними для наследников.
|
||||
maps.expand(&hierarchy, &owners);
|
||||
|
||||
// 5. Переписать определения и все ссылки.
|
||||
let mut local_counter = 0u64;
|
||||
for class in &mut program.classes {
|
||||
let orig_owner = class.this_class.clone();
|
||||
rewrite_class(class, &orig_owner, &maps, &cfg, &mut local_counter, ctx);
|
||||
}
|
||||
|
||||
// 6. Ширинирование доступа: после переезда в другие пакеты иначе
|
||||
// возможен IllegalAccessError.
|
||||
if cfg.rename.widen_access {
|
||||
widen_all(program);
|
||||
}
|
||||
|
||||
// 7. Маппинги в отчёт (обратная деобфускация stack trace'ей).
|
||||
for ((owner, name, desc), new_name) in maps.methods.iter() {
|
||||
ctx.mapping
|
||||
.methods
|
||||
.insert(method_key(owner, name, desc), new_name.clone());
|
||||
}
|
||||
for ((owner, name, desc), new_name) in maps.fields.iter() {
|
||||
ctx.mapping
|
||||
.fields
|
||||
.insert(field_key(owner, name, desc), new_name.clone());
|
||||
}
|
||||
for (old, new_name) in maps.classes.iter() {
|
||||
ctx.mapping.classes.insert(old.clone(), new_name.clone());
|
||||
}
|
||||
|
||||
let locked = analysis.locked_classes.len();
|
||||
if locked > 0 {
|
||||
ctx.note(format!("rename: {locked} классов в keep (не тронуты)"));
|
||||
}
|
||||
|
||||
Ok(PassReport {
|
||||
pass: PassId::Rename,
|
||||
transformed: renamed_classes + renamed_methods + renamed_fields,
|
||||
skipped: locked,
|
||||
notes: vec![format!(
|
||||
"классов: {renamed_classes}, методов: {renamed_methods}, полей: {renamed_fields}"
|
||||
)],
|
||||
})
|
||||
}
|
||||
}
|
||||
100
crates/noxis-passes/src/rename/rewrite/code.rs
Normal file
100
crates/noxis-passes/src/rename/rewrite/code.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
//! Переписывание тела метода: инструкции, кадры, handler'ы, атрибуты кода.
|
||||
|
||||
use noxis_classfile::{Code, Const, Insn};
|
||||
use noxis_core::{Config, Context};
|
||||
|
||||
use super::super::assign::{RenameMaps, rewrite_descriptor};
|
||||
use super::rewrite_attributes;
|
||||
use super::rewrite_frame;
|
||||
|
||||
pub(super) fn rewrite_code(
|
||||
code: &mut Code,
|
||||
maps: &RenameMaps,
|
||||
cfg: &Config,
|
||||
orig_owner: &str,
|
||||
ctx: &mut Context,
|
||||
local_counter: &mut u64,
|
||||
) {
|
||||
// Имена классов в кадрах тоже переименовываются, иначе верификатор
|
||||
// увидит старые типы (`compute_frames` их не пересчитывает здесь).
|
||||
for frame in code.frames.iter_mut() {
|
||||
rewrite_frame(frame, maps);
|
||||
}
|
||||
for insn in code.insns.iter_mut() {
|
||||
match insn {
|
||||
Insn::Const(Const::Class(name)) => {
|
||||
if let Some(mapped) = maps.classes.get(name.as_str()) {
|
||||
*name = mapped.clone();
|
||||
}
|
||||
}
|
||||
Insn::GetStatic {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
| Insn::PutStatic {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
| Insn::GetField {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
}
|
||||
| Insn::PutField {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
} => {
|
||||
if let Some(new_name) = maps.field(owner, name, descriptor) {
|
||||
*name = new_name;
|
||||
}
|
||||
if let Some(mapped) = maps.classes.get(owner.as_str()) {
|
||||
*owner = mapped.clone();
|
||||
}
|
||||
*descriptor = rewrite_descriptor(descriptor, &maps.classes);
|
||||
}
|
||||
Insn::Invoke {
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
..
|
||||
} => {
|
||||
if let Some(new_name) = maps.method(owner, name, descriptor) {
|
||||
*name = new_name;
|
||||
}
|
||||
if let Some(mapped) = maps.classes.get(owner.as_str()) {
|
||||
*owner = mapped.clone();
|
||||
}
|
||||
*descriptor = rewrite_descriptor(descriptor, &maps.classes);
|
||||
}
|
||||
Insn::New(name)
|
||||
| Insn::CheckCast(name)
|
||||
| Insn::InstanceOf(name)
|
||||
| Insn::ANewArray(name) => {
|
||||
if let Some(mapped) = maps.classes.get(name.as_str()) {
|
||||
*name = mapped.clone();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for handler in code.exception_table.iter_mut() {
|
||||
if let Some(catch) = handler.catch_type.clone()
|
||||
&& let Some(mapped) = maps.classes.get(catch.as_str())
|
||||
{
|
||||
handler.catch_type = Some(mapped.clone());
|
||||
}
|
||||
}
|
||||
|
||||
rewrite_attributes(
|
||||
&mut code.attributes,
|
||||
maps,
|
||||
cfg,
|
||||
orig_owner,
|
||||
ctx,
|
||||
local_counter,
|
||||
);
|
||||
}
|
||||
173
crates/noxis-passes/src/rename/rewrite/mod.rs
Normal file
173
crates/noxis-passes/src/rename/rewrite/mod.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
//! Переписывание одного класса: имена, дескрипторы, код, атрибуты.
|
||||
|
||||
mod code;
|
||||
|
||||
use noxis_classfile::{Attribute, ClassFile, StackMapFrame, VerificationType};
|
||||
use noxis_core::{Config, Context};
|
||||
|
||||
use super::assign::{RenameMaps, rewrite_descriptor};
|
||||
use crate::namegen::NameGenerator;
|
||||
use code::rewrite_code;
|
||||
|
||||
pub(super) fn rewrite_class(
|
||||
class: &mut ClassFile,
|
||||
orig_owner: &str,
|
||||
maps: &RenameMaps,
|
||||
cfg: &Config,
|
||||
local_counter: &mut u64,
|
||||
ctx: &mut Context,
|
||||
) {
|
||||
if let Some(new_name) = maps.classes.get(orig_owner) {
|
||||
class.this_class = new_name.clone();
|
||||
}
|
||||
if let Some(mapped) = class
|
||||
.super_class
|
||||
.as_ref()
|
||||
.and_then(|sup| maps.classes.get(sup))
|
||||
.cloned()
|
||||
{
|
||||
class.super_class = Some(mapped);
|
||||
}
|
||||
for iface in &mut class.interfaces {
|
||||
if let Some(mapped) = maps.classes.get(iface.as_str()) {
|
||||
*iface = mapped.clone();
|
||||
}
|
||||
}
|
||||
|
||||
for field in &mut class.fields {
|
||||
let key = (
|
||||
orig_owner.to_string(),
|
||||
field.name.clone(),
|
||||
field.descriptor.clone(),
|
||||
);
|
||||
if let Some(new_name) = maps.fields.get(&key) {
|
||||
field.name = new_name.clone();
|
||||
}
|
||||
field.descriptor = rewrite_descriptor(&field.descriptor, &maps.classes);
|
||||
rewrite_attributes(
|
||||
&mut field.attributes,
|
||||
maps,
|
||||
cfg,
|
||||
orig_owner,
|
||||
ctx,
|
||||
local_counter,
|
||||
);
|
||||
}
|
||||
|
||||
for method in &mut class.methods {
|
||||
let key = (
|
||||
orig_owner.to_string(),
|
||||
method.name.clone(),
|
||||
method.descriptor.clone(),
|
||||
);
|
||||
if let Some(new_name) = maps.methods.get(&key) {
|
||||
method.name = new_name.clone();
|
||||
}
|
||||
method.descriptor = rewrite_descriptor(&method.descriptor, &maps.classes);
|
||||
if let Some(code) = &mut method.code {
|
||||
rewrite_code(code, maps, cfg, orig_owner, ctx, local_counter);
|
||||
}
|
||||
rewrite_attributes(
|
||||
&mut method.attributes,
|
||||
maps,
|
||||
cfg,
|
||||
orig_owner,
|
||||
ctx,
|
||||
local_counter,
|
||||
);
|
||||
}
|
||||
|
||||
rewrite_attributes(
|
||||
&mut class.attributes,
|
||||
maps,
|
||||
cfg,
|
||||
orig_owner,
|
||||
ctx,
|
||||
local_counter,
|
||||
);
|
||||
}
|
||||
|
||||
fn rewrite_attributes(
|
||||
attributes: &mut [Attribute],
|
||||
maps: &RenameMaps,
|
||||
cfg: &Config,
|
||||
orig_owner: &str,
|
||||
ctx: &mut Context,
|
||||
local_counter: &mut u64,
|
||||
) {
|
||||
for attr in attributes.iter_mut() {
|
||||
match attr {
|
||||
Attribute::Signature(sig) => {
|
||||
*sig = rewrite_descriptor(sig, &maps.classes);
|
||||
}
|
||||
Attribute::Exceptions(list) => {
|
||||
for name in list {
|
||||
if let Some(mapped) = maps.classes.get(name.as_str()) {
|
||||
*name = mapped.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::InnerClasses(inner) => {
|
||||
for entry in inner.iter_mut() {
|
||||
if let Some(mapped) = maps.classes.get(entry.inner.as_str()) {
|
||||
entry.inner = mapped.clone();
|
||||
}
|
||||
if let Some(mapped) = entry
|
||||
.outer
|
||||
.as_ref()
|
||||
.and_then(|outer| maps.classes.get(outer))
|
||||
.cloned()
|
||||
{
|
||||
entry.outer = Some(mapped);
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::LocalVariableTable(vars) => {
|
||||
for var in vars.iter_mut() {
|
||||
var.descriptor = rewrite_descriptor(&var.descriptor, &maps.classes);
|
||||
if cfg.rename.rename_locals {
|
||||
*local_counter += 1;
|
||||
let mut names = NameGenerator::new(
|
||||
ctx.rng_nth("rename/locals", *local_counter),
|
||||
cfg.rename.local_style,
|
||||
);
|
||||
let new = names.next_name();
|
||||
ctx.mapping.locals.insert(
|
||||
format!("{orig_owner}/{}@{}", var.name, var.index),
|
||||
new.clone(),
|
||||
);
|
||||
var.name = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Имена классов внутри одного кадра (`Object(..)` в locals/stack).
|
||||
fn rewrite_frame(frame: &mut StackMapFrame, maps: &RenameMaps) {
|
||||
match frame {
|
||||
StackMapFrame::Same { .. } | StackMapFrame::Chop { .. } => {}
|
||||
StackMapFrame::SameLocals { local, .. } => rewrite_type(local, maps),
|
||||
StackMapFrame::Append { locals, .. } => rewrite_types(locals, maps),
|
||||
StackMapFrame::Full { locals, stack, .. } => {
|
||||
rewrite_types(locals, maps);
|
||||
rewrite_types(stack, maps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_types(types: &mut [VerificationType], maps: &RenameMaps) {
|
||||
for item in types.iter_mut() {
|
||||
rewrite_type(item, maps);
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrite_type(item: &mut VerificationType, maps: &RenameMaps) {
|
||||
if let VerificationType::Object(name) = item
|
||||
&& let Some(mapped) = maps.classes.get(name.as_str())
|
||||
{
|
||||
*name = mapped.clone();
|
||||
}
|
||||
}
|
||||
193
crates/noxis-passes/src/rename/tests.rs
Normal file
193
crates/noxis-passes/src/rename/tests.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
//! Тесты пасса переименования: keep, ссылки, семейства, алиасы.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use noxis_analysis::ClassHierarchy;
|
||||
use noxis_classfile::{
|
||||
ClassBuilder, ExceptionHandler, Insn, InvokeKind, Program, Resource, empty_code,
|
||||
};
|
||||
use noxis_core::{Config, Context, Pass, RenameConfig};
|
||||
|
||||
use super::RenamePass;
|
||||
use super::assign::{RenameMaps, rewrite_descriptor};
|
||||
|
||||
fn sample() -> Program {
|
||||
let main = ClassBuilder::new("app/Main", None)
|
||||
.method_with_code(
|
||||
0x0009,
|
||||
"main",
|
||||
"([Ljava/lang/String;)V",
|
||||
noxis_classfile::Code {
|
||||
max_stack: 4,
|
||||
max_locals: 1,
|
||||
insns: vec![
|
||||
Insn::Invoke {
|
||||
kind: InvokeKind::Static,
|
||||
owner: "app/Helper".into(),
|
||||
name: "make".into(),
|
||||
descriptor: "()Lapp/Helper;".into(),
|
||||
itf: false,
|
||||
},
|
||||
Insn::Pop,
|
||||
Insn::Return(None),
|
||||
],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
},
|
||||
)
|
||||
.build();
|
||||
let helper = ClassBuilder::new("app/Helper", None)
|
||||
.method_with_code(
|
||||
0x0009 | 0x0001,
|
||||
"make",
|
||||
"()Lapp/Helper;",
|
||||
noxis_classfile::Code {
|
||||
max_stack: 1,
|
||||
max_locals: 0,
|
||||
insns: vec![Insn::New("app/Helper".into()), Insn::Return(None)],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
},
|
||||
)
|
||||
.method_with_code(0x0002, "secret", "()V", empty_code(0))
|
||||
.build();
|
||||
Program {
|
||||
classes: vec![main, helper],
|
||||
resources: vec![Resource {
|
||||
path: "data.txt".into(),
|
||||
data: b"x".to_vec(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn run_rename(program: &mut Program) -> Context {
|
||||
let config = Config {
|
||||
seed: Some(9),
|
||||
keep: vec!["app/Main".to_string()],
|
||||
rename: RenameConfig {
|
||||
package_pool: 4,
|
||||
..RenameConfig::default()
|
||||
},
|
||||
..Config::default()
|
||||
};
|
||||
let mut ctx = Context::new(config);
|
||||
let report = RenamePass.apply(&mut ctx, program).expect("rename");
|
||||
assert!(report.transformed > 0, "ничего не переименовано");
|
||||
ctx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renames_unlocked_class_and_references() {
|
||||
let mut program = sample();
|
||||
let ctx = run_rename(&mut program);
|
||||
|
||||
assert_eq!(
|
||||
program.class("app/Main").map(|c| c.this_class.as_str()),
|
||||
Some("app/Main"),
|
||||
"keep-класс обязан остаться"
|
||||
);
|
||||
let helper = program
|
||||
.classes
|
||||
.iter()
|
||||
.find(|c| c.this_class != "app/Main")
|
||||
.cloned()
|
||||
.expect("helper");
|
||||
assert_ne!(helper.this_class, "app/Helper");
|
||||
|
||||
let main = program.class("app/Main").expect("main");
|
||||
let code = main.methods[0].code.as_ref().expect("code");
|
||||
let (new_owner, new_desc) = match &code.insns[0] {
|
||||
Insn::Invoke {
|
||||
owner, descriptor, ..
|
||||
} => (owner.clone(), descriptor.clone()),
|
||||
other => panic!("ожидался Invoke, получено {other:?}"),
|
||||
};
|
||||
assert_eq!(new_owner, helper.this_class);
|
||||
assert!(new_desc.contains(&helper.this_class), "desc: {new_desc}");
|
||||
|
||||
let renamed_secret = helper
|
||||
.methods
|
||||
.iter()
|
||||
.find(|m| m.name != "make")
|
||||
.map(|m| m.name.clone())
|
||||
.expect("secret переименован");
|
||||
assert_ne!(renamed_secret, "secret");
|
||||
assert!(!ctx.mapping.methods.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptor_rewrite_handles_arrays() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("a/b/C".to_string(), "p/Q".to_string());
|
||||
map.insert("a/b/D".to_string(), "p/R".to_string());
|
||||
assert_eq!(
|
||||
rewrite_descriptor("(La/b/C;[La/b/D;)La/b/C;", &map),
|
||||
"(Lp/Q;[Lp/R;)Lp/Q;"
|
||||
);
|
||||
assert_eq!(rewrite_descriptor("([[I)V", &map), "([[I)V");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exception_handler_type_is_rewritten() {
|
||||
let mut program = sample();
|
||||
program
|
||||
.classes
|
||||
.push(ClassBuilder::new("app/Boom", None).build());
|
||||
program.classes[1].methods[0]
|
||||
.code
|
||||
.as_mut()
|
||||
.expect("code")
|
||||
.exception_table
|
||||
.push(ExceptionHandler {
|
||||
start: 0,
|
||||
end: 1,
|
||||
handler: 2,
|
||||
catch_type: Some("app/Boom".into()),
|
||||
});
|
||||
let ctx = run_rename(&mut program);
|
||||
let new_boom = ctx.mapping.class("app/Boom").expect("boom renamed");
|
||||
let helper = program
|
||||
.classes
|
||||
.iter()
|
||||
.find(|c| c.this_class != "app/Main")
|
||||
.expect("helper");
|
||||
let handler = &helper.methods[0]
|
||||
.code
|
||||
.as_ref()
|
||||
.expect("code")
|
||||
.exception_table[0];
|
||||
assert_eq!(handler.catch_type.as_deref(), Some(new_boom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subclass_gets_alias_for_inherited_member() {
|
||||
let base = ClassBuilder::new("p/Base", None)
|
||||
.method_with_code(0x0009, "shared", "()V", empty_code(0))
|
||||
.build();
|
||||
let sub = ClassBuilder::new("p/Sub", Some("p/Base")).build();
|
||||
let program = Program {
|
||||
classes: vec![base, sub],
|
||||
resources: Vec::new(),
|
||||
};
|
||||
let hierarchy = ClassHierarchy::build(&program);
|
||||
let mut maps = RenameMaps::default();
|
||||
maps.methods.insert(
|
||||
("p/Base".into(), "shared".into(), "()V".into()),
|
||||
"zz".into(),
|
||||
);
|
||||
let owners = vec!["p/Base".to_string(), "p/Sub".to_string()];
|
||||
maps.expand(&hierarchy, &owners);
|
||||
|
||||
// Вызов через подкласс обязан находить новое имя.
|
||||
assert_eq!(
|
||||
maps.method("p/Base", "shared", "()V").as_deref(),
|
||||
Some("zz")
|
||||
);
|
||||
assert_eq!(maps.method("p/Sub", "shared", "()V").as_deref(), Some("zz"));
|
||||
// Чужого члена не выдумываем.
|
||||
assert_eq!(maps.method("p/Sub", "other", "()V"), None);
|
||||
}
|
||||
170
crates/noxis-passes/src/resources/mod.rs
Normal file
170
crates/noxis-passes/src/resources/mod.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
//! Пасс ресурсов: патч текстовых файлов архива по маппингу классов.
|
||||
//!
|
||||
//! Без этого обфускация Fabric-мода просто не стартует: `fabric.mod.json`
|
||||
//! хранит main-class точкой, `*.mixins.json` — пути внутренними именами,
|
||||
//! refmap и access widener — тоже. Материал: `info/evo-weaknesses.md` §3/§8
|
||||
//! (у разобранного образца ресурсы вообще не трогали).
|
||||
//!
|
||||
//! Замена **границная**, а не подстрочная: `org/example/Foo` не должен
|
||||
//! совпасть внутри `myorg/example/FooBar`. Правило то же, что у проверенного
|
||||
//! референса: сначала дескрипторный вид `Lpkg/Class;`, затем внутренний вид
|
||||
//! с `/`, затем бинарный с `.`, длинные имена первыми.
|
||||
|
||||
/// Интеграционные тесты пасса.
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use noxis_classfile::Program;
|
||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||
|
||||
/// Расширения текстовых ресурсов, которые имеют смысл переписывать.
|
||||
const TEXT_EXTS: [&str; 9] = [
|
||||
".json",
|
||||
".mcmeta",
|
||||
".properties",
|
||||
".txt",
|
||||
".xml",
|
||||
".yml",
|
||||
".yaml",
|
||||
".cfg",
|
||||
".accesswidener",
|
||||
];
|
||||
|
||||
/// Service loader'ы: файла без расширения, но каждая строка — имя класса.
|
||||
const SERVICES_PREFIX: &str = "META-INF/services/";
|
||||
|
||||
/// Пасс патча ресурсов.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct ResourcesPass;
|
||||
|
||||
impl Pass for ResourcesPass {
|
||||
fn id(&self) -> PassId {
|
||||
PassId::Resources
|
||||
}
|
||||
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||
let map = ctx.mapping.classes.clone();
|
||||
if map.is_empty() {
|
||||
return Ok(PassReport {
|
||||
pass: PassId::Resources,
|
||||
transformed: 0,
|
||||
skipped: program.resources.len(),
|
||||
notes: vec!["маппинг классов пуст — ресурсы не тронуты".into()],
|
||||
});
|
||||
}
|
||||
|
||||
let mut patched = 0usize;
|
||||
let mut skipped = 0usize;
|
||||
|
||||
for resource in &mut program.resources {
|
||||
if !is_text(&resource.path) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
let Ok(text) = std::str::from_utf8(&resource.data) else {
|
||||
skipped += 1;
|
||||
continue;
|
||||
};
|
||||
let updated = remap_text(text, &map);
|
||||
if updated == text {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
resource.data = updated.into_bytes();
|
||||
patched += 1;
|
||||
}
|
||||
|
||||
let mut notes = Vec::new();
|
||||
if patched > 0 {
|
||||
notes.push(format!("патч ресурсов: {patched}, пропущено: {skipped}"));
|
||||
}
|
||||
|
||||
Ok(PassReport {
|
||||
pass: PassId::Resources,
|
||||
transformed: patched,
|
||||
skipped,
|
||||
notes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Текстовый ли это ресурс.
|
||||
///
|
||||
/// Бинарии (картинки, `.class` в `versions/`, нативные библиотеки) не
|
||||
/// трогаем: их переписывание ломает содержимое молча.
|
||||
pub fn is_text(path: &str) -> bool {
|
||||
if path.starts_with(SERVICES_PREFIX) {
|
||||
return !path.ends_with('/');
|
||||
}
|
||||
let lower = path.to_ascii_lowercase();
|
||||
TEXT_EXTS.iter().any(|ext| lower.ends_with(ext))
|
||||
}
|
||||
|
||||
/// Переписывает все ссылки на классы в тексте по таблице `old → new`.
|
||||
pub fn remap_text(text: &str, map: &BTreeMap<String, String>) -> String {
|
||||
if map.is_empty() {
|
||||
return text.to_string();
|
||||
}
|
||||
|
||||
// Длинные имена первыми: иначе `org/example/Foo` совпадёт внутри
|
||||
// `org/example/FooBar` ещё до того, как до него дойдёт очередь.
|
||||
let mut pairs: Vec<(&String, &String)> = map.iter().collect();
|
||||
pairs.sort_by_key(|(from, _)| Reverse(from.len()));
|
||||
|
||||
let mut out = text.to_string();
|
||||
for (from, to) in pairs {
|
||||
out = out.replace(&format!("L{from};"), &format!("L{to};"));
|
||||
out = replace_delimited(&out, from, to, false);
|
||||
out = replace_delimited(&out, &from.replace('/', "."), &to.replace('/', "."), true);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Заменяет все вхождения `old`, отделённые от surrounding текста границами
|
||||
/// имени. `dotted` выбирает набор запрещённых соседей: `/` или `.`.
|
||||
fn replace_delimited(text: &str, old: &str, new: &str, dotted: bool) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut rest = text;
|
||||
|
||||
while let Some(at) = rest.find(old) {
|
||||
let before = &rest[..at];
|
||||
let after = &rest[at + old.len()..];
|
||||
|
||||
if is_boundary(before.chars().next_back(), dotted)
|
||||
&& is_boundary(after.chars().next(), dotted)
|
||||
{
|
||||
out.push_str(before);
|
||||
out.push_str(new);
|
||||
rest = after;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Совпадение частичное — пропускаем первый символ и ищем дальше.
|
||||
let mut chars = rest[at..].chars();
|
||||
out.push_str(before);
|
||||
match chars.next() {
|
||||
Some(first) => {
|
||||
out.push(first);
|
||||
rest = chars.as_str();
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str(rest);
|
||||
out
|
||||
}
|
||||
|
||||
/// `true`, если символ слева/справа допускает начало/конец имени класса.
|
||||
fn is_boundary(neighbour: Option<char>, dotted: bool) -> bool {
|
||||
let Some(ch) = neighbour else {
|
||||
return true;
|
||||
};
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '$' {
|
||||
return false;
|
||||
}
|
||||
if dotted { ch != '.' } else { ch != '/' }
|
||||
}
|
||||
160
crates/noxis-passes/src/resources/tests.rs
Normal file
160
crates/noxis-passes/src/resources/tests.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
//! Тесты пасса ресурсов: границы совпадений и патч содержимого.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{Program, Resource};
|
||||
use noxis_core::{Config, Context, Pass};
|
||||
|
||||
use super::{ResourcesPass, is_text, remap_text};
|
||||
|
||||
fn mapping() -> std::collections::BTreeMap<String, String> {
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert("org/example/Foo".to_string(), "a/b/C".to_string());
|
||||
map.insert("org/example/FooBar".to_string(), "a/b/D".to_string());
|
||||
map
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptor_form_is_replaced() {
|
||||
let text = "Lorg/example/Foo; [Lorg/example/FooBar;";
|
||||
assert_eq!(
|
||||
remap_text(text, &mapping()),
|
||||
"La/b/C; [La/b/D;",
|
||||
"дескрипторный вид обязан заменяться без проверки границ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_form_respects_boundaries() {
|
||||
let map = mapping();
|
||||
assert_eq!(remap_text("owner=Lorg/example/Foo;", &map), "owner=La/b/C;");
|
||||
assert_eq!(
|
||||
remap_text("myorg/example/Foo", &map),
|
||||
"myorg/example/Foo",
|
||||
"совпадение внутри более длинного пути не трогаем"
|
||||
);
|
||||
assert_eq!(
|
||||
remap_text("org/example/FooBaz", &map),
|
||||
"org/example/FooBaz",
|
||||
"суффикс не должен совпасть с префиксом"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dotted_form_respects_boundaries() {
|
||||
let map = mapping();
|
||||
assert_eq!(
|
||||
remap_text("\"org.example.Foo\" => \"x\"", &map),
|
||||
"\"a.b.C\" => \"x\""
|
||||
);
|
||||
assert_eq!(
|
||||
remap_text("myorg.example.Foo", &map),
|
||||
"myorg.example.Foo",
|
||||
"точка внутри имени не является границей"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longest_name_wins() {
|
||||
assert_eq!(
|
||||
remap_text("Lorg/example/FooBar;", &mapping()),
|
||||
"La/b/D;",
|
||||
"длинное имя обрабатывается раньше короткого"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_map_is_a_noop() {
|
||||
let text = "Lorg/example/Foo; и org.example.Foo";
|
||||
assert_eq!(remap_text(text, &Default::default()), text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_text_survives_unrelated_substitution() {
|
||||
let text = "комментарий: Лорем_ипсум — org/example/Bar";
|
||||
assert_eq!(remap_text(text, &mapping()), text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_detection_covers_known_formats() {
|
||||
assert!(is_text("fabric.mod.json"));
|
||||
assert!(is_text("demo.mixins.json"));
|
||||
assert!(is_text("META-INF/services/org.example.Loader"));
|
||||
assert!(is_text("mod.accesswidener"));
|
||||
assert!(is_text("META-INF/versions/1.20/mod.json"));
|
||||
|
||||
assert!(!is_text("assets/tex.png"));
|
||||
assert!(!is_text("lib/native.so"));
|
||||
assert!(!is_text("META-INF/services/"));
|
||||
}
|
||||
|
||||
fn context_with_mapping() -> Context {
|
||||
let mut ctx = Context::new(Config {
|
||||
seed: Some(3),
|
||||
..Config::default()
|
||||
});
|
||||
ctx.mapping
|
||||
.classes
|
||||
.insert("org/example/Foo".into(), "a/b/C".into());
|
||||
ctx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_patches_text_and_counts_the_rest() {
|
||||
let mut program = Program {
|
||||
classes: Vec::new(),
|
||||
resources: vec![
|
||||
Resource {
|
||||
path: "fabric.mod.json".into(),
|
||||
data: br#"{"entrypoints":{"main":["org.example.Foo"]}}"#.to_vec(),
|
||||
},
|
||||
Resource {
|
||||
path: "META-INF/services/org.example.Foo".into(),
|
||||
data: b"# keep\n".to_vec(),
|
||||
},
|
||||
Resource {
|
||||
path: "assets/icon.png".into(),
|
||||
data: vec![0x89, 0x50, 0x4E, 0x47],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let mut ctx = context_with_mapping();
|
||||
let report = ResourcesPass
|
||||
.apply(&mut ctx, &mut program)
|
||||
.expect("resources");
|
||||
|
||||
assert_eq!(report.transformed, 1, "патчим только JSON");
|
||||
assert_eq!(report.skipped, 2);
|
||||
|
||||
let json = std::str::from_utf8(&program.resources[0].data).unwrap();
|
||||
assert!(
|
||||
json.contains("a.b.C"),
|
||||
"binary-имя в JSON не заменено: {json}"
|
||||
);
|
||||
assert!(!json.contains("org.example.Foo"), "старое имя осталось");
|
||||
assert_eq!(program.resources[2].data, vec![0x89, 0x50, 0x4E, 0x47]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_is_a_noop_without_a_mapping() {
|
||||
let mut program = Program {
|
||||
classes: Vec::new(),
|
||||
resources: vec![Resource {
|
||||
path: "fabric.mod.json".into(),
|
||||
data: br#"{"x":"org.example.Foo"}"#.to_vec(),
|
||||
}],
|
||||
};
|
||||
|
||||
let mut ctx = Context::new(Config::default());
|
||||
let report = ResourcesPass
|
||||
.apply(&mut ctx, &mut program)
|
||||
.expect("resources");
|
||||
|
||||
assert_eq!(report.transformed, 0);
|
||||
assert_eq!(report.skipped, 1);
|
||||
assert!(
|
||||
!report.notes.is_empty(),
|
||||
"должна быть заметка о пустом маппинге"
|
||||
);
|
||||
}
|
||||
60
crates/noxis-passes/src/strings/crypt.rs
Normal file
60
crates/noxis-passes/src/strings/crypt.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//! Шифрование строк: чанки, salt под схему, последовательность вызова.
|
||||
//!
|
||||
//! Один `Const::String` превращается в `ldc cipher; ldc salt; invokestatic
|
||||
//! d` на чанк; чанки склеиваются `String.concat` (без `StringBuilder` —
|
||||
//! меньше инструкций и нет вызовов конструктора).
|
||||
|
||||
use noxis_classfile::{Const, Insn, InvokeKind};
|
||||
use noxis_runtime::{DECODER_DESC, DECODER_METHOD};
|
||||
|
||||
/// Разбить строку на чанки по границе символов (1–3 штуки).
|
||||
pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.len() <= split_above.max(1) {
|
||||
return vec![text.to_string()];
|
||||
}
|
||||
let count = chars.len().div_ceil(split_above);
|
||||
let count = count.clamp(1, 3);
|
||||
let per = chars.len().div_ceil(count);
|
||||
chars
|
||||
.chunks(per)
|
||||
.map(|chunk| chunk.iter().collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Salt, попадающий в нужную схему: `salt % total == index`.
|
||||
///
|
||||
/// Salt всегда неотрицательный (Java `%` сохраняет знак — отрицательный salt
|
||||
/// выбрал бы не ту ветку декодера). `index` обязан быть меньше `total`.
|
||||
pub(super) fn salt_for_scheme(raw: u64, index: usize, total: usize) -> i32 {
|
||||
let total = total.max(1) as u64;
|
||||
let index = (index as u64) % total;
|
||||
let rounds = (i32::MAX as u64 - index) / total + 1;
|
||||
(index + (raw >> 33) % rounds * total) as i32
|
||||
}
|
||||
|
||||
/// Вызовы декодера на чанки + склейка (`[(cipher, salt)]`).
|
||||
pub(super) fn decode_sequence(owner: &str, parts: &[(String, i32)]) -> Vec<Insn> {
|
||||
let mut out = Vec::with_capacity(parts.len() * 4);
|
||||
for (position, (cipher, salt)) in parts.iter().enumerate() {
|
||||
out.push(Insn::Const(Const::String(cipher.clone())));
|
||||
out.push(Insn::Const(Const::Int(*salt)));
|
||||
out.push(Insn::Invoke {
|
||||
kind: InvokeKind::Static,
|
||||
owner: owner.to_string(),
|
||||
name: DECODER_METHOD.to_string(),
|
||||
descriptor: DECODER_DESC.to_string(),
|
||||
itf: false,
|
||||
});
|
||||
if position > 0 {
|
||||
out.push(Insn::Invoke {
|
||||
kind: InvokeKind::Virtual,
|
||||
owner: "java/lang/String".to_string(),
|
||||
name: "concat".to_string(),
|
||||
descriptor: "(Ljava/lang/String;)Ljava/lang/String;".to_string(),
|
||||
itf: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
193
crates/noxis-passes/src/strings/mod.rs
Normal file
193
crates/noxis-passes/src/strings/mod.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
//! Пасс шифрования строк: `Const::String` → вызовы рантайм-декодера.
|
||||
//!
|
||||
//! Схема строки выбирается ротацией (`salt % n`), salt детерминирован
|
||||
//! (KDF или RNG-поток — см. `derive_key`). Декодер генерируется один на
|
||||
//! программу и только если была зашифрована хотя бы одна строка.
|
||||
|
||||
mod crypt;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use noxis_analysis::ClassHierarchy;
|
||||
use noxis_classfile::{Const, Insn, Program, compute_frames};
|
||||
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
|
||||
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
|
||||
|
||||
use crate::namegen::NameGenerator;
|
||||
use crypt::{decode_sequence, salt_for_scheme, split_chunks};
|
||||
|
||||
/// Пасс шифрования строк.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct StringsPass;
|
||||
|
||||
impl Pass for StringsPass {
|
||||
fn id(&self) -> PassId {
|
||||
PassId::Strings
|
||||
}
|
||||
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||
let cfg = ctx.config.strings.clone();
|
||||
let mut schemes = Vec::with_capacity(cfg.schemes.len());
|
||||
for name in &cfg.schemes {
|
||||
schemes.push(Scheme::parse(name).map_err(|e| pass_error(e.to_string()))?);
|
||||
}
|
||||
if schemes.is_empty() {
|
||||
return Err(pass_error("нет схем шифрования"));
|
||||
}
|
||||
// Фаза 1: собрать все шифруемые строки (позиция + текст).
|
||||
let mut targets: Vec<(usize, usize, usize, String)> = Vec::new();
|
||||
let mut skipped = 0usize;
|
||||
for (ci, class) in program.classes.iter().enumerate() {
|
||||
for (mi, method) in class.methods.iter().enumerate() {
|
||||
let Some(code) = method.code.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
for (ii, insn) in code.insns.iter().enumerate() {
|
||||
if let Insn::Const(Const::String(text)) = insn {
|
||||
if text.chars().count() >= cfg.min_len && !is_skipped(text, &cfg.skip) {
|
||||
targets.push((ci, mi, ii, text.clone()));
|
||||
} else {
|
||||
skipped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if targets.is_empty() {
|
||||
return Ok(report(
|
||||
0,
|
||||
skipped,
|
||||
vec!["нет строк для шифрования".to_string()],
|
||||
));
|
||||
}
|
||||
// Фаза 2: зашифровать и подставить последовательности вызовов.
|
||||
let master = ctx.master_seed;
|
||||
let mut rng = ctx.rng("strings");
|
||||
let owner = decoder_name(ctx, program);
|
||||
let mut chunk_counter = 0usize;
|
||||
let mut salt_counter = 0u64;
|
||||
let mut replacements: BTreeMap<(usize, usize, usize), Vec<Insn>> = BTreeMap::new();
|
||||
for (ci, mi, ii, text) in &targets {
|
||||
let mut parts = Vec::new();
|
||||
for chunk in split_chunks(text, cfg.split_above) {
|
||||
let scheme = chunk_counter % schemes.len();
|
||||
let raw = next_salt(&mut rng, master, &mut salt_counter, cfg.derive_key);
|
||||
let salt = salt_for_scheme(raw, scheme, schemes.len());
|
||||
let blob = encrypt(schemes[scheme], master, salt, &chunk);
|
||||
parts.push((blob.cipher, salt));
|
||||
chunk_counter += 1;
|
||||
}
|
||||
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts));
|
||||
}
|
||||
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
||||
for (ci, class) in program.classes.iter_mut().enumerate() {
|
||||
for (mi, method) in class.methods.iter_mut().enumerate() {
|
||||
let Some(code) = method.code.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let mut fresh = Vec::with_capacity(code.insns.len());
|
||||
let mut touched = false;
|
||||
for (ii, insn) in code.insns.drain(..).enumerate() {
|
||||
if let Some(sequence) = replacements.remove(&(ci, mi, ii)) {
|
||||
fresh.extend(sequence);
|
||||
touched = true;
|
||||
} else {
|
||||
fresh.push(insn);
|
||||
}
|
||||
}
|
||||
if touched {
|
||||
code.max_stack = code.max_stack.saturating_add(2);
|
||||
mutated.push((ci, mi));
|
||||
}
|
||||
code.insns = fresh;
|
||||
}
|
||||
}
|
||||
// Фаза 3: класс-декодер один на программу.
|
||||
let scheme_names: Vec<&str> = cfg.schemes.iter().map(String::as_str).collect();
|
||||
let decoder =
|
||||
decoder_class(&owner, master, &scheme_names).map_err(|e| pass_error(e.to_string()))?;
|
||||
program.classes.push(decoder);
|
||||
ctx.note(format!("декодер строк: {owner}"));
|
||||
// Фаза 4: фреймы затронутых методов.
|
||||
let hierarchy = ClassHierarchy::build(program);
|
||||
for (ci, mi) in &mutated {
|
||||
let (name, snapshot) = {
|
||||
let class = program
|
||||
.classes
|
||||
.get(*ci)
|
||||
.ok_or_else(|| pass_error("класс пропал"))?;
|
||||
(
|
||||
class.this_class.clone(),
|
||||
class
|
||||
.methods
|
||||
.get(*mi)
|
||||
.cloned()
|
||||
.ok_or_else(|| pass_error("метод пропал"))?,
|
||||
)
|
||||
};
|
||||
let frames = compute_frames(&name, &snapshot, &hierarchy)
|
||||
.map_err(|e| pass_error(e.to_string()))?;
|
||||
program
|
||||
.classes
|
||||
.get_mut(*ci)
|
||||
.and_then(|c| c.methods.get_mut(*mi))
|
||||
.and_then(|m| m.code.as_mut())
|
||||
.ok_or_else(|| pass_error("метод пропал"))?
|
||||
.frames = frames;
|
||||
}
|
||||
Ok(report(
|
||||
targets.len(),
|
||||
skipped,
|
||||
vec![format!(
|
||||
"зашифровано строк: {}, декодер: {owner}",
|
||||
targets.len()
|
||||
)],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn report(transformed: usize, skipped: usize, notes: Vec<String>) -> PassReport {
|
||||
PassReport {
|
||||
pass: PassId::Strings,
|
||||
transformed,
|
||||
skipped,
|
||||
notes,
|
||||
}
|
||||
}
|
||||
|
||||
fn pass_error(msg: impl Into<String>) -> CoreError {
|
||||
CoreError::pass(PassId::Strings, msg)
|
||||
}
|
||||
|
||||
/// Строка подпадает под `skip`-паттерны конфига.
|
||||
fn is_skipped(text: &str, skip: &[String]) -> bool {
|
||||
skip.iter().any(|pattern| text.contains(pattern))
|
||||
}
|
||||
|
||||
/// Salt чанка: KDF от позиции или RNG-поток (см. `derive_key`).
|
||||
fn next_salt(rng: &mut noxis_core::Rng, master: u64, counter: &mut u64, derive: bool) -> u64 {
|
||||
let value = if derive {
|
||||
derive_seed(master, "strings/salt", *counter)
|
||||
} else {
|
||||
rng.next_u64()
|
||||
};
|
||||
*counter += 1;
|
||||
value
|
||||
}
|
||||
|
||||
/// Детерминированное имя декодера без коллизий с классами программы.
|
||||
fn decoder_name(ctx: &mut Context, program: &Program) -> String {
|
||||
let mut names = NameGenerator::new(ctx.rng("strings/decoder"), NameStyle::ShortLatin);
|
||||
for class in &program.classes {
|
||||
names.reserve(class.this_class.clone());
|
||||
}
|
||||
loop {
|
||||
let owner = decoder_owner(&format!("{}/{}", names.next_name(), names.next_name()));
|
||||
if program.class(&owner).is_none() {
|
||||
return owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
crates/noxis-passes/src/strings/tests.rs
Normal file
129
crates/noxis-passes/src/strings/tests.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
//! Тесты пасса строк: подстановка, чанки, детерминизм шифротекста.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Program, Version};
|
||||
use noxis_core::{Config, Context};
|
||||
use noxis_runtime::{Scheme, encrypt};
|
||||
|
||||
use super::StringsPass;
|
||||
use super::crypt::{decode_sequence, salt_for_scheme, split_chunks};
|
||||
use noxis_core::Pass;
|
||||
|
||||
fn sample() -> Program {
|
||||
let code = Code {
|
||||
max_stack: 1,
|
||||
max_locals: 0,
|
||||
insns: vec![
|
||||
Insn::Const(Const::String("hello world".to_string())),
|
||||
Insn::Const(Const::String("x".to_string())),
|
||||
Insn::Const(Const::String("y".repeat(100))),
|
||||
Insn::Const(Const::Int(5)),
|
||||
Insn::Return(None),
|
||||
],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
Program {
|
||||
classes: vec![
|
||||
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(0x0009, "run", "()V", code)
|
||||
.build(),
|
||||
],
|
||||
resources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Короткие строки не режутся, длинные — на границе символов.
|
||||
#[test]
|
||||
fn chunks_split_by_chars() {
|
||||
assert_eq!(split_chunks("abc", 64), vec!["abc".to_string()]);
|
||||
let chunks = split_chunks(&"y".repeat(100), 64);
|
||||
assert_eq!(chunks.len(), 2);
|
||||
assert_eq!(chunks.concat().len(), 100);
|
||||
}
|
||||
|
||||
/// Salt всегда неотрицателен и попадает в схему.
|
||||
#[test]
|
||||
fn salt_hits_scheme() {
|
||||
for raw in [0u64, 1, 7, u64::MAX, 123456789] {
|
||||
for (index, total) in [(0, 1), (0, 2), (1, 2), (2, 3)] {
|
||||
let salt = salt_for_scheme(raw, index, total);
|
||||
assert!(salt >= 0, "отрицательный salt");
|
||||
assert_eq!(salt as usize % total, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Пасс: строки заменены вызовами, шифротекст воспроизводим, декодер есть.
|
||||
#[test]
|
||||
fn strings_replaced_and_deterministic() {
|
||||
let mut program = sample();
|
||||
let config = Config {
|
||||
seed: Some(7),
|
||||
..Config::default()
|
||||
};
|
||||
let mut ctx = Context::new(config.clone());
|
||||
let report = StringsPass.apply(&mut ctx, &mut program).expect("strings");
|
||||
|
||||
assert_eq!(report.transformed, 2);
|
||||
assert_eq!(report.skipped, 1);
|
||||
// Декодер добавлен последним.
|
||||
assert_eq!(program.classes.len(), 2);
|
||||
let decoder = program.classes.last().expect("декодер");
|
||||
assert!(decoder.methods.iter().any(|m| m.name == "d"));
|
||||
|
||||
// Исходных строк не осталось (шифры — длинные Const::String — на месте);
|
||||
// короткая на месте.
|
||||
let code = program.classes[0].methods[0].code.as_ref().expect("код");
|
||||
let kept: Vec<&String> = code
|
||||
.insns
|
||||
.iter()
|
||||
.filter_map(|insn| match insn {
|
||||
Insn::Const(Const::String(s)) => Some(s),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(!kept.contains(&&"hello world".to_string()));
|
||||
assert!(!kept.iter().any(|s| *s == &"y".repeat(100)));
|
||||
assert!(kept.contains(&&"x".to_string()));
|
||||
|
||||
// Каждый вызов сверяется с прямым шифрованием (тот же сид → те же блобы).
|
||||
let schemes = [Scheme::XorSplitmix, Scheme::XteaCtr];
|
||||
let mut triples = Vec::new();
|
||||
let mut window: Vec<&Insn> = Vec::new();
|
||||
for insn in &code.insns {
|
||||
window.push(insn);
|
||||
if window.len() > 3 {
|
||||
window.remove(0);
|
||||
}
|
||||
if let [
|
||||
Insn::Const(Const::String(cipher)),
|
||||
Insn::Const(Const::Int(salt)),
|
||||
Insn::Invoke { name, .. },
|
||||
] = &window[..]
|
||||
&& name == "d"
|
||||
{
|
||||
triples.push((cipher.clone(), *salt));
|
||||
}
|
||||
}
|
||||
// "hello world" (1 чанк) + "y"*100 (2 чанка).
|
||||
assert_eq!(triples.len(), 3);
|
||||
let originals = ["hello world".to_string(), "y".repeat(50), "y".repeat(50)];
|
||||
for ((cipher, salt), original) in triples.iter().zip(originals) {
|
||||
let scheme = schemes[*salt as usize % schemes.len()];
|
||||
let blob = encrypt(scheme, 7, *salt, &original);
|
||||
assert_eq!(cipher, &blob.cipher);
|
||||
}
|
||||
// Склейка чанков через `concat` — ровно одна; конструктор жив.
|
||||
assert_eq!(
|
||||
code.insns
|
||||
.iter()
|
||||
.filter(|insn| matches!(insn, Insn::Invoke { name, .. } if name == "concat"))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(!decode_sequence("o", &[("c".to_string(), 1)]).is_empty());
|
||||
}
|
||||
18
crates/noxis-runtime/Cargo.toml
Normal file
18
crates/noxis-runtime/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "noxis-runtime"
|
||||
description = "Схемы шифрования строк и генерация рантайм-декодеров для JVM"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
noxis-classfile = { workspace = true }
|
||||
noxis-core = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis-runtime/build.rs
Normal file
3
crates/noxis-runtime/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
108
crates/noxis-runtime/src/codec.rs
Normal file
108
crates/noxis-runtime/src/codec.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
//! Кодирование шифротекста в вид, допустимый для `CONSTANT_String`.
|
||||
//!
|
||||
//! LDC работает только со строками (modified UTF-8), поэтому байты шифротекста
|
||||
//! кодируем ASCII-алфавитом: без Base64 из JDK и без лишних зависимостей.
|
||||
|
||||
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/// Ошибка кодирования.
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum CodecError {
|
||||
#[error("некорректный base64")]
|
||||
InvalidBase64,
|
||||
}
|
||||
|
||||
/// Base64 (стандартный алфавит, без переносов строк).
|
||||
pub fn encode(data: &[u8]) -> String {
|
||||
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
|
||||
for chunk in data.chunks(3) {
|
||||
let b0 = chunk[0] as u32;
|
||||
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
|
||||
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
|
||||
let triple = (b0 << 16) | (b1 << 8) | b2;
|
||||
out.push(ALPHABET[(triple >> 18) as usize & 63] as char);
|
||||
out.push(ALPHABET[(triple >> 12) as usize & 63] as char);
|
||||
if chunk.len() > 1 {
|
||||
out.push(ALPHABET[(triple >> 6) as usize & 63] as char);
|
||||
} else {
|
||||
out.push('=');
|
||||
}
|
||||
if chunk.len() > 2 {
|
||||
out.push(ALPHABET[triple as usize & 63] as char);
|
||||
} else {
|
||||
out.push('=');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn val(c: u8) -> Option<u32> {
|
||||
match c {
|
||||
b'A'..=b'Z' => Some(u32::from(c - b'A')),
|
||||
b'a'..=b'z' => Some(u32::from(c - b'a') + 26),
|
||||
b'0'..=b'9' => Some(u32::from(c - b'0') + 52),
|
||||
b'+' => Some(62),
|
||||
b'/' => Some(63),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Base64 → байты.
|
||||
pub fn decode(text: &str) -> Result<Vec<u8>, CodecError> {
|
||||
let bytes = text.as_bytes();
|
||||
if !bytes.len().is_multiple_of(4) {
|
||||
return Err(CodecError::InvalidBase64);
|
||||
}
|
||||
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
|
||||
for chunk in bytes.chunks(4) {
|
||||
let mut acc = 0u32;
|
||||
let mut pads = 0;
|
||||
for (i, &c) in chunk.iter().enumerate() {
|
||||
if c == b'=' {
|
||||
if i < 2 {
|
||||
return Err(CodecError::InvalidBase64);
|
||||
}
|
||||
pads += 1;
|
||||
acc <<= 6;
|
||||
continue;
|
||||
}
|
||||
let v = val(c).ok_or(CodecError::InvalidBase64)?;
|
||||
acc = (acc << 6) | v;
|
||||
}
|
||||
out.push((acc >> 16) as u8);
|
||||
if pads < 2 {
|
||||
out.push((acc >> 8) as u8);
|
||||
}
|
||||
if pads < 1 {
|
||||
out.push(acc as u8);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
for data in [
|
||||
&b""[..],
|
||||
b"a",
|
||||
b"ab",
|
||||
b"abc",
|
||||
b"abcd",
|
||||
&[0u8, 255, 128, 7][..],
|
||||
] {
|
||||
let enc = encode(data);
|
||||
assert_eq!(decode(&enc).expect("decode"), data, "data={data:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_garbage() {
|
||||
assert!(decode("abc").is_err());
|
||||
assert!(decode("!!!!").is_err());
|
||||
}
|
||||
}
|
||||
196
crates/noxis-runtime/src/decoder/kdf.rs
Normal file
196
crates/noxis-runtime/src/decoder/kdf.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
//! KDF декодера: FNV-1a64 поверх `master || domain || salt`.
|
||||
//!
|
||||
//! Зеркало `noxis_core::derive_seed` (тот же FNV-1a64, те же домены).
|
||||
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
|
||||
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
|
||||
//!
|
||||
//! Здесь же — строитель тела метода и короткие эмиссии (общие для всех
|
||||
//! ветвей декодера).
|
||||
|
||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
|
||||
|
||||
/// Строитель тела метода: метки, слоты, короткие эмиссии.
|
||||
pub(super) struct Gen {
|
||||
pub insns: Vec<Insn>,
|
||||
labels: u32,
|
||||
pub slots: u16,
|
||||
}
|
||||
|
||||
impl Gen {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
insns: Vec::new(),
|
||||
labels: 0,
|
||||
// Слоты 0 (`cipher`) и 1 (`salt`) — параметры метода.
|
||||
slots: 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn label(&mut self) -> Label {
|
||||
let label = self.labels;
|
||||
self.labels += 1;
|
||||
label
|
||||
}
|
||||
|
||||
/// Занять слоты (`wide` — пара под `long`).
|
||||
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
|
||||
let slot = self.slots;
|
||||
self.slots += if wide { 2 } else { 1 };
|
||||
slot
|
||||
}
|
||||
|
||||
pub(super) fn emit(&mut self, insn: Insn) {
|
||||
self.insns.push(insn);
|
||||
}
|
||||
|
||||
pub(super) fn mark(&mut self, label: Label) {
|
||||
self.emit(Insn::Mark(label));
|
||||
}
|
||||
}
|
||||
|
||||
/// Короткие эмиссии операндов (`writer` выбирает `iconst`/`bipush` сам).
|
||||
pub(super) fn c_int(value: i32) -> Insn {
|
||||
Insn::Const(Const::Int(value))
|
||||
}
|
||||
|
||||
pub(super) fn c_long(value: i64) -> Insn {
|
||||
Insn::Const(Const::Long(value))
|
||||
}
|
||||
|
||||
pub(super) fn c_str(value: &str) -> Insn {
|
||||
Insn::Const(Const::String(value.to_string()))
|
||||
}
|
||||
|
||||
pub(super) fn load(slot: u16, kind: SlotKind) -> Insn {
|
||||
Insn::Load { slot, kind }
|
||||
}
|
||||
|
||||
pub(super) fn store(slot: u16, kind: SlotKind) -> Insn {
|
||||
Insn::Store { slot, kind }
|
||||
}
|
||||
|
||||
pub(super) fn call(owner: &str, name: &str, descriptor: &str, kind: InvokeKind) -> Insn {
|
||||
Insn::Invoke {
|
||||
kind,
|
||||
owner: owner.to_string(),
|
||||
name: name.to_string(),
|
||||
descriptor: descriptor.to_string(),
|
||||
itf: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// FNV-1a64 init (знаковое представление для `ldc2_w`).
|
||||
const FNV_INIT: i64 = 0xCBF2_9CE4_8422_2325u64 as i64;
|
||||
/// FNV-1a64 prime.
|
||||
const FNV_PRIME: i64 = 0x0000_0100_0000_01B3;
|
||||
|
||||
/// Вычислить `derive_seed(master, domain, salt)`; вернуть слот пары.
|
||||
pub(super) fn emit_kdf(asm: &mut Gen, master: u16, domain: &str) -> u16 {
|
||||
let input = asm.alloc(false);
|
||||
build_input(asm, master, domain, input);
|
||||
let hash = asm.alloc(true);
|
||||
let index = asm.alloc(false);
|
||||
asm.emit(c_long(FNV_INIT));
|
||||
asm.emit(store(hash, SlotKind::Long));
|
||||
asm.emit(c_int(0));
|
||||
asm.emit(store(index, SlotKind::Int));
|
||||
let again = asm.label();
|
||||
let done = asm.label();
|
||||
asm.mark(again);
|
||||
asm.emit(load(index, SlotKind::Int));
|
||||
asm.emit(load(input, SlotKind::Ref));
|
||||
asm.emit(Insn::ArrayLength);
|
||||
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ge), done));
|
||||
asm.emit(load(hash, SlotKind::Long));
|
||||
asm.emit(load(input, SlotKind::Ref));
|
||||
asm.emit(load(index, SlotKind::Int));
|
||||
asm.emit(Insn::ArrayLoad(noxis_classfile::ArrayKind::Byte));
|
||||
asm.emit(Insn::Convert(noxis_classfile::Convert::I2L));
|
||||
asm.emit(c_long(0xFF));
|
||||
asm.emit(Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::And,
|
||||
});
|
||||
asm.emit(Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Xor,
|
||||
});
|
||||
asm.emit(store(hash, SlotKind::Long));
|
||||
asm.emit(load(hash, SlotKind::Long));
|
||||
asm.emit(c_long(FNV_PRIME));
|
||||
asm.emit(Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Mul,
|
||||
});
|
||||
asm.emit(store(hash, SlotKind::Long));
|
||||
asm.emit(Insn::Iinc { index, delta: 1 });
|
||||
asm.emit(Insn::Goto(again));
|
||||
asm.mark(done);
|
||||
let out = asm.alloc(true);
|
||||
asm.emit(load(hash, SlotKind::Long));
|
||||
asm.emit(store(out, SlotKind::Long));
|
||||
out
|
||||
}
|
||||
|
||||
/// Собрать вход `master || domain || salt` в слот `input`.
|
||||
///
|
||||
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian —
|
||||
/// поэтому оба `long` разворачиваются (`Long.reverseBytes`).
|
||||
fn build_input(asm: &mut Gen, master: u16, domain: &str, input: u16) {
|
||||
asm.emit(c_int(16 + domain.len() as i32));
|
||||
asm.emit(call(
|
||||
"java/nio/ByteBuffer",
|
||||
"allocate",
|
||||
"(I)Ljava/nio/ByteBuffer;",
|
||||
InvokeKind::Static,
|
||||
));
|
||||
asm.emit(Insn::Dup);
|
||||
asm.emit(load(master, SlotKind::Long));
|
||||
asm.emit(call(
|
||||
"java/lang/Long",
|
||||
"reverseBytes",
|
||||
"(J)J",
|
||||
InvokeKind::Static,
|
||||
));
|
||||
asm.emit(call(
|
||||
"java/nio/ByteBuffer",
|
||||
"putLong",
|
||||
"(J)Ljava/nio/ByteBuffer;",
|
||||
InvokeKind::Virtual,
|
||||
));
|
||||
asm.emit(Insn::Dup);
|
||||
asm.emit(c_str(domain));
|
||||
asm.emit(call(
|
||||
"java/lang/String",
|
||||
"getBytes",
|
||||
"()[B",
|
||||
InvokeKind::Virtual,
|
||||
));
|
||||
asm.emit(call(
|
||||
"java/nio/ByteBuffer",
|
||||
"put",
|
||||
"([B)Ljava/nio/ByteBuffer;",
|
||||
InvokeKind::Virtual,
|
||||
));
|
||||
asm.emit(load(1, SlotKind::Int));
|
||||
asm.emit(Insn::Convert(noxis_classfile::Convert::I2L));
|
||||
asm.emit(call(
|
||||
"java/lang/Long",
|
||||
"reverseBytes",
|
||||
"(J)J",
|
||||
InvokeKind::Static,
|
||||
));
|
||||
asm.emit(call(
|
||||
"java/nio/ByteBuffer",
|
||||
"putLong",
|
||||
"(J)Ljava/nio/ByteBuffer;",
|
||||
InvokeKind::Virtual,
|
||||
));
|
||||
asm.emit(call(
|
||||
"java/nio/ByteBuffer",
|
||||
"array",
|
||||
"()[B",
|
||||
InvokeKind::Virtual,
|
||||
));
|
||||
asm.emit(store(input, SlotKind::Ref));
|
||||
}
|
||||
186
crates/noxis-runtime/src/decoder/mod.rs
Normal file
186
crates/noxis-runtime/src/decoder/mod.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
//! Рантайм-декодер: ABI, который получает байткод после подстановки строк.
|
||||
//!
|
||||
//! ABI v1 (стабильный, его же нужно повторить на Java side):
|
||||
//!
|
||||
//! ```text
|
||||
//! static String d(String cipher, int salt)
|
||||
//! ```
|
||||
//!
|
||||
//! - `cipher` — base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
|
||||
//! - `salt` — индекс строки, по нему детерминированно выводятся key и nonce;
|
||||
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
|
||||
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
|
||||
//!
|
||||
//! Класс версии 49: циклы верифицируются выводом типов без `StackMapTable`
|
||||
//! (проверено загрузкой v49 на Java 21), кадры не генерируются.
|
||||
|
||||
mod kdf;
|
||||
mod xor;
|
||||
mod xtea;
|
||||
|
||||
use noxis_classfile::{
|
||||
Arith, BranchCond, ClassBuilder, ClassFile, Code, Cond, Insn, InvokeKind, Prim, SlotKind,
|
||||
Version,
|
||||
};
|
||||
|
||||
use crate::scheme::Scheme;
|
||||
|
||||
use kdf::{Gen, c_int, c_long, c_str, call, load, store};
|
||||
|
||||
/// Имя метода декодера.
|
||||
pub const DECODER_METHOD: &str = "d";
|
||||
/// Дескриптор метода декодера.
|
||||
pub const DECODER_DESC: &str = "(Ljava/lang/String;I)Ljava/lang/String;";
|
||||
|
||||
/// Ошибка генерации рантайма.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RuntimeError {
|
||||
#[error("не реализовано: {0}")]
|
||||
Unimplemented(&'static str),
|
||||
#[error("нет активных схем шифрования")]
|
||||
NoSchemes,
|
||||
}
|
||||
|
||||
/// Сгенерировать класс-декодер (мастер-секрет + расшифровка всех схем).
|
||||
pub fn decoder_class(
|
||||
internal_name: &str,
|
||||
master: u64,
|
||||
schemes: &[&str],
|
||||
) -> Result<ClassFile, RuntimeError> {
|
||||
if schemes.is_empty() {
|
||||
return Err(RuntimeError::NoSchemes);
|
||||
}
|
||||
let mut parsed = Vec::with_capacity(schemes.len());
|
||||
for name in schemes {
|
||||
parsed.push(Scheme::parse(name).map_err(|_| RuntimeError::NoSchemes)?);
|
||||
}
|
||||
let mut asm = Gen::new();
|
||||
let master_slot = emit_master(&mut asm, master);
|
||||
let key = kdf::emit_kdf(&mut asm, master_slot, "string/key");
|
||||
let nonce = kdf::emit_kdf(&mut asm, master_slot, "string/nonce");
|
||||
let data = emit_base64(&mut asm);
|
||||
let len = emit_len(&mut asm, data);
|
||||
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
|
||||
emit_epilogue(&mut asm, data);
|
||||
let code = Code {
|
||||
// Точный пик — 10 слотов (замер 08.10.2026); запас без вреда:
|
||||
// лимит обязан лишь покрывать фактическую глубину.
|
||||
max_stack: 12,
|
||||
max_locals: asm.slots,
|
||||
insns: asm.insns,
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
Ok(ClassBuilder::new(internal_name.to_string(), None)
|
||||
.version(Version {
|
||||
minor: 0,
|
||||
major: 49,
|
||||
})
|
||||
.method_with_code(0x0009, DECODER_METHOD, DECODER_DESC, code)
|
||||
.build())
|
||||
}
|
||||
|
||||
/// Имя класса-декодера для прогона: детерминированно от сида, но выглядит
|
||||
/// как обычный обфусцированный класс.
|
||||
pub fn decoder_owner(seed_name: &str) -> String {
|
||||
seed_name.to_string()
|
||||
}
|
||||
|
||||
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
|
||||
/// мастер-секрету (вторая — `master ^ k1`, первая — потоковая).
|
||||
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
|
||||
use noxis_core::Rng;
|
||||
|
||||
let mut rng = Rng::from_seed(master ^ 0x9E37_79B9_7F4A_7C15);
|
||||
let first = rng.next_u64() as i64;
|
||||
let second = (master ^ first as u64) as i64;
|
||||
let slot = asm.alloc(true);
|
||||
asm.emit(c_long(first));
|
||||
asm.emit(c_long(second));
|
||||
asm.emit(Insn::Binary {
|
||||
prim: Prim::Long,
|
||||
op: Arith::Xor,
|
||||
});
|
||||
asm.emit(store(slot, SlotKind::Long));
|
||||
slot
|
||||
}
|
||||
|
||||
/// `data = Base64.getDecoder().decode(cipher)`; возвращает слот массива.
|
||||
fn emit_base64(asm: &mut Gen) -> u16 {
|
||||
let slot = asm.alloc(false);
|
||||
asm.emit(call(
|
||||
"java/util/Base64",
|
||||
"getDecoder",
|
||||
"()Ljava/util/Base64$Decoder;",
|
||||
InvokeKind::Static,
|
||||
));
|
||||
asm.emit(load(0, SlotKind::Ref));
|
||||
asm.emit(call(
|
||||
"java/util/Base64$Decoder",
|
||||
"decode",
|
||||
"(Ljava/lang/String;)[B",
|
||||
InvokeKind::Virtual,
|
||||
));
|
||||
asm.emit(store(slot, SlotKind::Ref));
|
||||
slot
|
||||
}
|
||||
|
||||
/// `len = data.length`; возвращает слот.
|
||||
fn emit_len(asm: &mut Gen, data: u16) -> u16 {
|
||||
let slot = asm.alloc(false);
|
||||
asm.emit(load(data, SlotKind::Ref));
|
||||
asm.emit(Insn::ArrayLength);
|
||||
asm.emit(store(slot, SlotKind::Int));
|
||||
slot
|
||||
}
|
||||
|
||||
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 — без диспетчера).
|
||||
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
|
||||
if schemes.len() == 1 {
|
||||
dispatch_branch(asm, schemes[0], key, nonce, data, len);
|
||||
return;
|
||||
}
|
||||
let done = asm.label();
|
||||
for (index, scheme) in schemes.iter().enumerate() {
|
||||
if index + 1 == schemes.len() {
|
||||
break;
|
||||
}
|
||||
let next = asm.label();
|
||||
asm.emit(load(1, SlotKind::Int));
|
||||
asm.emit(c_int(schemes.len() as i32));
|
||||
asm.emit(Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Rem,
|
||||
});
|
||||
asm.emit(c_int(index as i32));
|
||||
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ne), next));
|
||||
dispatch_branch(asm, *scheme, key, nonce, data, len);
|
||||
asm.emit(Insn::Goto(done));
|
||||
asm.mark(next);
|
||||
}
|
||||
dispatch_branch(asm, schemes[schemes.len() - 1], key, nonce, data, len);
|
||||
asm.mark(done);
|
||||
}
|
||||
|
||||
fn dispatch_branch(asm: &mut Gen, scheme: Scheme, key: u16, nonce: u16, data: u16, len: u16) {
|
||||
match scheme {
|
||||
Scheme::XorSplitmix => xor::emit_xor(asm, key, nonce, data, len),
|
||||
Scheme::XteaCtr => xtea::emit_xtea(asm, key, nonce, data, len),
|
||||
}
|
||||
}
|
||||
|
||||
/// Эпилог: `return new String(data, "UTF-8")`.
|
||||
fn emit_epilogue(asm: &mut Gen, data: u16) {
|
||||
asm.emit(Insn::New("java/lang/String".to_string()));
|
||||
asm.emit(Insn::Dup);
|
||||
asm.emit(load(data, SlotKind::Ref));
|
||||
asm.emit(c_str("UTF-8"));
|
||||
asm.emit(call(
|
||||
"java/lang/String",
|
||||
"<init>",
|
||||
"([BLjava/lang/String;)V",
|
||||
InvokeKind::Special,
|
||||
));
|
||||
asm.emit(Insn::AReturn);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue