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:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

19
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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 в репозитории.

View 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 }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View 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"));
}
}

View 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(&current) {
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(&current)
&& class.method(name, descriptor).is_some()
{
out.push(current.clone());
}
stack.extend(self.parents(&current));
}
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
);
}
}

View 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(&regex::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"));
}
}

View 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};

View 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 }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View 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),
}

View 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
}

View 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()
}

View 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()),
}
}

View 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))
}
}

View 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}"),
}
}

View 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,
}
}

View 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(&params),
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),
}
}

View 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(&params), 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(())
}
}
}

View 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)
}
}

View 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()
}

View 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)?))
}
}

View 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)?))
}
}

View 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)?))
}
}

View 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} вне тела метода")))
}

View 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()))
}

View 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 }
}

View 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<_, _>>()?,
})
}

View 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:?}"
))),
}
}

View 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,
}
}

View 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;

View 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:?} указывает на несовместимую ссылку"
)))
}
}

View 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 &param.name {
None => 0,
Some(name) => pool.utf8(name)?,
},
access_flags: MethodAccessFlags::from_bits_truncate(param.access_flags),
})
}

View 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 },
})
}

View 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))
}

View 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")))
}

View 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}"))
}

View 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,
})
}

View 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())
}

View 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,
}

View 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,
}
}
}

View 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,
};

View 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());
}
}

View 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()
}
}

View 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,
};
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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(())
}

View 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
}

View 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}"
);
}

View 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 });
}

View 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 }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View 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());
}
}

View 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,
}
}
}

View 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(),
}
}
}

View 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};

View 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"));
}
}

View 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);
}
}

View 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));
}
}

View 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;
}
}

View 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>>());
}
}

View 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 }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View 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(_)));
}
}

View 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};

View 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}"),
}
}
}

View 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"]
);
}
}

View 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 }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View 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),
]
}

View 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)
}

View 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);
}

View 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);
}
}

View 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));
}
}

View 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",
];

View 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");
}
}
}

View 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 &notes {
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);
}
}

View 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)
}

View 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
}

View 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}"
)],
})
}
}

View 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,
);
}

View 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();
}
}

View 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);
}

View 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 != '/' }
}

View 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(),
"должна быть заметка о пустом маппинге"
);
}

View 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
}

View 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;
}
}
}

View 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());
}

View 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 }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View 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());
}
}

View 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));
}

View 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