commit a641e69455a5eadd2e177c267592dad4327adfa3 Author: loki5512344 Date: Thu Oct 8 22:01:03 2026 +0200 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..083ba40 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..364e024 --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b80072e --- /dev/null +++ b/Cargo.toml @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e344efa --- /dev/null +++ b/LICENSE @@ -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. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..b059baf --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,17 @@ +# NOTICE — сторонние компоненты + +Проект `noxis` распространяется по лицензии AGPL-3.0-only +(см. [`LICENSE`](LICENSE)). +Ниже — компоненты third-party, которые используются напрямую и чьи +лицензионные условия обязательны к сохранению. + +## ristretto_classfile + +- **Источник:** +- **Версия:** 0.34.0 (зависимость crates.io, код не вендорится) +- **Лицензия:** Apache-2.0 OR MIT (на выбор) +- **Использование:** чтение и запись `.class` (`format::parse` / + `format::write`), приёмочная верификация `verify_class(Strict)`. + +Компонент подключается как обычная cargo-зависимость; исходные тексты и +лицензии хранятся в реестре (`~/.cargo/registry`) и в `Cargo.lock`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..76e15e0 --- /dev/null +++ b/README.md @@ -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 в репозитории. diff --git a/crates/noxis-analysis/Cargo.toml b/crates/noxis-analysis/Cargo.toml new file mode 100644 index 0000000..40224a6 --- /dev/null +++ b/crates/noxis-analysis/Cargo.toml @@ -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 } diff --git a/crates/noxis-analysis/build.rs b/crates/noxis-analysis/build.rs new file mode 100644 index 0000000..d39e2d0 --- /dev/null +++ b/crates/noxis-analysis/build.rs @@ -0,0 +1,3 @@ +fn main() { + noxis_structure_check::check(); +} diff --git a/crates/noxis-analysis/src/entrypoints.rs b/crates/noxis-analysis/src/entrypoints.rs new file mode 100644 index 0000000..f559d1f --- /dev/null +++ b/crates/noxis-analysis/src/entrypoints.rs @@ -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, + /// `native`-методы: имя связано с JNI-схемой (`Java_pkg_Class_method`). + pub native_methods: BTreeSet<(String, String, String)>, + /// Интерфейсы из `META-INF/services/*`. + pub service_interfaces: BTreeSet, + /// Имена, встречающиеся в рефлексивных вызовах (`Class.forName` и т.п.). + pub reflective_names: BTreeSet, + /// Имена членов (`getMethod`/`getField`-литералы): их нельзя переименовывать. + pub reflective_member_names: BTreeSet, + /// Поля с флагом `transient`+`serialVersionUID`-контекст: сериализуемые классы + /// обязаны сохранить имя класса и полей (иначе ломается Java-сериализация). + pub serializable_classes: BTreeSet, +} + +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/` → внутреннее имя интерфейса. +fn collect_service_interfaces(program: &Program) -> BTreeSet { + 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")); + } +} diff --git a/crates/noxis-analysis/src/hierarchy.rs b/crates/noxis-analysis/src/hierarchy.rs new file mode 100644 index 0000000..6a57ea1 --- /dev/null +++ b/crates/noxis-analysis/src/hierarchy.rs @@ -0,0 +1,250 @@ +//! Иерархия классов: наследование, переопределения, безопасность переименования. + +use std::collections::{HashMap, HashSet}; + +use noxis_classfile::Program; + +/// Узел иерархии: суперкласс и интерфейсы (по внутренним именам). +#[derive(Debug, Clone, Default)] +pub struct HierarchyNode { + pub super_class: Option, + pub interfaces: Vec, +} + +/// Построенная иерархия классов. +#[derive(Debug, Clone, Default)] +pub struct ClassHierarchy { + nodes: HashMap, +} + +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 { + 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 { + let mut out = Vec::new(); + if let Some(node) = self.nodes.get(internal_name) { + out.extend(node.super_class.iter().cloned()); + out.extend(node.interfaces.iter().cloned()); + } + out + } + + /// Является ли `child` подклассом `parent` (в пределах программы). + /// + /// Неполная иерархия (внешние зависимости не грузим) — это осознанно: + /// обфускатор работает по артефакту, а не по всему classpath. + pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool { + if child == parent { + return true; + } + let mut seen = HashSet::new(); + let mut stack = vec![child.to_string()]; + while let Some(current) = stack.pop() { + if !seen.insert(current.clone()) { + continue; + } + for p in self.parents(¤t) { + if p == parent { + return true; + } + stack.push(p); + } + } + false + } + + /// Все классы программы, которые наследуют `parent` (включая его самого). + pub fn descendants(&self, parent: &str) -> Vec { + 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 { + let mut out = Vec::new(); + let mut seen = HashSet::new(); + let mut stack = vec![start.to_string()]; + while let Some(current) = stack.pop() { + if !seen.insert(current.clone()) { + continue; + } + if let Some(class) = program.class(¤t) + && class.method(name, descriptor).is_some() + { + out.push(current.clone()); + } + stack.extend(self.parents(¤t)); + } + out + } +} + +impl noxis_classfile::Hierarchy for ClassHierarchy { + fn superclass(&self, internal_name: &str) -> Option { + self.superclass(internal_name) + } +} +/// Почему метод нельзя/можно переименовывать (аналог `InheritanceAnalyzer`). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RenameSafety { + /// Переименовывать можно: это точка, где имя не видно снаружи. + Safe, + /// Конструктор/статический блок: имя зафиксировано форматом JVM. + FixedName, + /// Виртуальный метод, который может переопределяться вне артефакта. + VirtualExternal, + /// Рефлексия/сериализация может обратиться по имени. + ReferencedByName, + /// Метод не найден в классе. + Missing, +} + +/// Оценка безопасности переименования метода. +/// +/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`): +/// - ``/`` — никогда; +/// - `private`/`static`/`final`/`` внутри финального класса — можно; +/// - `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 == "" || name == "" { + 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, "", "()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", "", "()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 + ); + } +} diff --git a/crates/noxis-analysis/src/keep.rs b/crates/noxis-analysis/src/keep.rs new file mode 100644 index 0000000..2addd9f --- /dev/null +++ b/crates/noxis-analysis/src/keep.rs @@ -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 { + let mut out = String::from("^"); + let chars: Vec = pattern.chars().collect(); + let mut i = 0; + while i < chars.len() { + match chars[i] { + '*' => { + if i + 1 < chars.len() && chars[i + 1] == '*' { + out.push_str(".*"); + i += 2; + } else { + out.push_str("[^/]*"); + i += 1; + } + } + '?' => { + out.push_str("[^/]"); + i += 1; + } + c => { + out.push_str(®ex::escape(&c.to_string())); + i += 1; + } + } + } + out.push('$'); + Regex::new(&out) +} + +/// Правила «не трогать»: явные паттерны конфига + entrypoint'ы. +#[derive(Debug, Default, Clone)] +pub struct KeepRules { + class_patterns: Vec, + resource_patterns: Vec, + /// Точные имена, которые нельзя трогать (entrypoint'ы, рефлексия, ...). + fixed: std::collections::BTreeSet, + /// Имена членов, защищённые рефлексией. + fixed_members: std::collections::BTreeSet, +} + +impl KeepRules { + /// Собрать правила из конфига. + pub fn from_config(config: &Config) -> Result { + 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) { + self.fixed_members.insert(name.into()); + } + + /// Добавить точное имя класса (из анализа программы). + pub fn keep_exact(&mut self, internal_name: impl Into) { + 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, +} + +impl Analysis { + /// Прогнать весь анализ по программе. + pub fn run(program: &Program, config: &Config) -> Result { + 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")); + } +} diff --git a/crates/noxis-analysis/src/lib.rs b/crates/noxis-analysis/src/lib.rs new file mode 100644 index 0000000..fb6fac5 --- /dev/null +++ b/crates/noxis-analysis/src/lib.rs @@ -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}; diff --git a/crates/noxis-classfile/Cargo.toml b/crates/noxis-classfile/Cargo.toml new file mode 100644 index 0000000..3a88b3c --- /dev/null +++ b/crates/noxis-classfile/Cargo.toml @@ -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 } diff --git a/crates/noxis-classfile/build.rs b/crates/noxis-classfile/build.rs new file mode 100644 index 0000000..d39e2d0 --- /dev/null +++ b/crates/noxis-classfile/build.rs @@ -0,0 +1,3 @@ +fn main() { + noxis_structure_check::check(); +} diff --git a/crates/noxis-classfile/src/error.rs b/crates/noxis-classfile/src/error.rs new file mode 100644 index 0000000..28f9689 --- /dev/null +++ b/crates/noxis-classfile/src/error.rs @@ -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), +} diff --git a/crates/noxis-classfile/src/format/frames/emit.rs b/crates/noxis-classfile/src/format/frames/emit.rs new file mode 100644 index 0000000..3715578 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/emit.rs @@ -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], + targets: &BTreeSet, + initial: &[VerificationType], + merger: &Merger<'_>, +) -> Result, ClassfileError> { + let mut out = Vec::new(); + let mut previous_locals = initial.to_vec(); + let mut previous_index: Option = 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 { + 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 +} diff --git a/crates/noxis-classfile/src/format/frames/flow.rs b/crates/noxis-classfile/src/format/frames/flow.rs new file mode 100644 index 0000000..7a10222 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/flow.rs @@ -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, 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, + merger: &Merger<'_>, +) -> Result, 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, + handlers: &[HandlerRange], + merger: &Merger<'_>, +) -> Result, 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); + +/// Handler'ы в номерах инструкций для рёбер исключений. +pub(super) fn handler_ranges( + table: &[ExceptionHandler], + labels: &BTreeMap, + merger: &Merger<'_>, +) -> Result, ClassfileError> { + table + .iter() + .map(|handler| { + let index_of = |label: Label| -> Result { + 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() +} diff --git a/crates/noxis-classfile/src/format/frames/merge/descs.rs b/crates/noxis-classfile/src/format/frames/merge/descs.rs new file mode 100644 index 0000000..dd9c243 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/merge/descs.rs @@ -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, +) -> Result { + 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, 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>, Option>); + +/// Дескриптор метода: слоты параметров и слоты возврата (`None` — `void`). +pub(crate) fn method_slots(descriptor: &str) -> Result { + 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()), + } +} diff --git a/crates/noxis-classfile/src/format/frames/merge/mod.rs b/crates/noxis-classfile/src/format/frames/merge/mod.rs new file mode 100644 index 0000000..28c3c20 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/merge/mod.rs @@ -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; +} + +/// Контекст слияния: имена для ошибок + иерархия для 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, + incoming: State, + ) -> Result { + 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 { + 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 == "" { + 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 { + 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::>()?, + stack: first + .stack + .iter() + .zip(&second.stack) + .map(|(a, b)| self.lub_types(a, b)) + .collect::>()?, + }) + } + + fn lub_types( + &self, + first: &VerificationType, + second: &VerificationType, + ) -> Result { + 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)) + } +} diff --git a/crates/noxis-classfile/src/format/frames/mod.rs b/crates/noxis-classfile/src/format/frames/mod.rs new file mode 100644 index 0000000..07eec95 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/mod.rs @@ -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, + }, + Full { + offset_delta: u32, + locals: Vec, + stack: Vec, + }, +} + +/// Вычислить фреймы для тела метода после всех трансформаций. +/// +/// Кадры нужны только классам версии ≥ 50; `jsr`/`ret` отвергаются (такие +/// классы старше и кадры им не нужны). Иерархия — для LUB ссылочных типов. +pub fn compute_frames( + class: &str, + method: &Method, + hierarchy: &dyn Hierarchy, +) -> Result, 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> = 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, + merger: &Merger<'_>, +) -> Result, ClassfileError> { + use crate::format::write::{code, pool::Pool}; + + let mut pool = Pool::new(); + let resolve = |label: Label| -> Result { + 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, + handlers: &[HandlerRange], + states: &mut [Option], +) -> Result<(), ClassfileError> { + let mut queue: VecDeque = 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], + queue: &mut VecDeque, + 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) -> 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}"), + } +} diff --git a/crates/noxis-classfile/src/format/frames/tests.rs b/crates/noxis-classfile/src/format/frames/tests.rs new file mode 100644 index 0000000..7e3bcad --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/tests.rs @@ -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 { + None + } +} + +fn code_method( + name: &str, + descriptor: &str, + max_stack: u16, + max_locals: u16, + insns: Vec, + table: Vec, +) -> 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, + } +} diff --git a/crates/noxis-classfile/src/format/frames/xfer/calls.rs b/crates/noxis-classfile/src/format/frames/xfer/calls.rs new file mode 100644 index 0000000..98f4950 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/xfer/calls.rs @@ -0,0 +1,133 @@ +//! Вызовы и константы: дескрипторы, ``, слоты значений. + +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, DescError> { + Ok(match value { + Const::Int(_) => vec![VerificationType::Integer], + Const::Long(_) => vec![VerificationType::Long, VerificationType::Top], + Const::Float(_) => vec![VerificationType::Float], + Const::Double(_) => vec![VerificationType::Double, VerificationType::Top], + Const::String(_) => vec![VerificationType::Object("java/lang/String".to_string())], + Const::Class(_) => vec![VerificationType::Object("java/lang/Class".to_string())], + Const::MethodType(_) => vec![VerificationType::Object( + "java/lang/invoke/MethodType".to_string(), + )], + Const::MethodHandle { .. } => vec![VerificationType::Object( + "java/lang/invoke/MethodHandle".to_string(), + )], + Const::Dynamic { descriptor, .. } => field_slots(descriptor)?, + }) +} + +pub(super) fn invoke( + class: &str, + method: &str, + state: &mut State, + kind: InvokeKind, + owner: &str, + name: &str, + descriptor: &str, +) -> Result<(), ClassfileError> { + let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?; + super::stack::pop_slots( + state, + count_slots(¶ms), + class, + method, + "аргументы вызова", + )?; + let mut receiver = None; + if kind != InvokeKind::Static { + receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop(); + } + // `` инициализирует получателя во ВСЕХ его копиях. + if name == "" { + 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]) -> usize { + params.iter().map(Vec::len).sum() +} + +/// Заменить все вхождения типа новым (инициализация через ``). +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 { + 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), + } +} diff --git a/crates/noxis-classfile/src/format/frames/xfer/mod.rs b/crates/noxis-classfile/src/format/frames/xfer/mod.rs new file mode 100644 index 0000000..1814794 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/xfer/mod.rs @@ -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, + pub stack: Vec, +} + +/// Применить инструкцию к состоянию (на месте). +pub(super) fn transfer( + class: &str, + method: &str, + insn: &Insn, + offset: u16, + state: &mut State, +) -> Result<(), ClassfileError> { + match insn { + Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))), + Insn::Nop => Ok(()), + Insn::AconstNull => push(state, vec![VerificationType::Null]), + Insn::Const(value) => { + let slots = const_slots(value).map_err(|e| context(class, method, e))?; + push(state, slots) + } + Insn::Load { slot, kind } => { + let value = local_get(state, *slot, class, method)?; + let _ = kind; + push(state, wide_push(value)) + } + Insn::Store { slot, kind } => { + let slots = usize::from(*kind == SlotKind::Long || *kind == SlotKind::Double) + 1; + let taken = pop_slots(state, slots, class, method, "store")?; + store_local(state, *slot, *kind, taken, class, method) + } + Insn::Iinc { index, .. } => { + let current = local_get(state, *index, class, method)?; + if current != VerificationType::Integer { + return Err(invalid(class, method, "iinc не по int")); + } + Ok(()) + } + Insn::ArrayLoad(kind) => { + pop_slots(state, 1, class, method, "индекс массива")?; + let array = pop_slots(state, 1, class, method, "массив")?.remove(0); + push(state, vec![array_element(&array, *kind)]) + } + Insn::ArrayStore(kind) => { + let count = match kind { + ArrayKind::Long | ArrayKind::Double => 2, + _ => 1, + }; + pop_slots(state, count, class, method, "значение")?; + pop_slots(state, 1, class, method, "индекс массива")?; + pop_slots(state, 1, class, method, "массив")?; + Ok(()) + } + Insn::Pop => { + let top = pop_slots(state, 1, class, method, "pop")?.remove(0); + if top == VerificationType::Top { + return Err(invalid(class, method, "pop половины long/double")); + } + Ok(()) + } + Insn::Pop2 => { + pop_slots(state, 2, class, method, "pop2")?; + Ok(()) + } + Insn::Dup => dup(state, class, method, &[1], &[1, 1]), + Insn::DupX1 => dup(state, class, method, &[2, 1], &[1, 2, 1]), + Insn::DupX2 => dup(state, class, method, &[3, 2, 1], &[1, 3, 2, 1]), + Insn::Dup2 => dup2(state, class, method, false, false), + Insn::Dup2X1 => dup2(state, class, method, true, false), + Insn::Dup2X2 => dup2(state, class, method, true, true), + Insn::Swap => { + let mut taken = pop_slots(state, 2, class, method, "swap")?; + if taken.contains(&VerificationType::Top) { + return Err(invalid(class, method, "swap с половиной")); + } + taken.reverse(); + push(state, taken) + } + Insn::Binary { prim, op } => { + let slots = prim_slots(*prim); + if *op == Arith::Neg { + pop_slots(state, slots as usize, class, method, "neg")?; + } else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) { + // Сдвиг берёт (значение, int), а не два значения. + pop_slots(state, slots as usize + 1, class, method, "shift")?; + } else { + pop_slots(state, 2 * slots as usize, class, method, "arith")?; + } + push(state, prim_push(*prim)) + } + Insn::Cmp { prim, .. } => { + pop_slots(state, 2 * prim_slots(*prim) as usize, class, method, "cmp")?; + push(state, vec![VerificationType::Integer]) + } + Insn::Convert(op) => { + let (src, dst) = convert_shapes(*op); + pop_slots(state, src, class, method, "convert")?; + push(state, prim_push(dst)) + } + Insn::Branch(cond, _) => { + let count = match cond { + BranchCond::Zero(..) | BranchCond::Null { .. } => 1, + BranchCond::Int(..) | BranchCond::Ref { .. } => 2, + }; + pop_slots(state, count, class, method, "branch")?; + Ok(()) + } + Insn::Goto(_) => Ok(()), + Insn::Jsr(_) | Insn::Ret(_) => Err(invalid( + class, + method, + "подпрограммы jsr/ret не поддерживаются", + )), + Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => { + pop_slots(state, 1, class, method, "switch")?; + Ok(()) + } + Insn::Return(prim) => { + if let Some(prim) = prim { + pop_slots(state, prim_slots(*prim) as usize, class, method, "return")?; + } + Ok(()) + } + Insn::AReturn => { + pop_slots(state, 1, class, method, "areturn")?; + Ok(()) + } + Insn::GetStatic { descriptor, .. } => { + let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?; + push(state, slots) + } + Insn::PutStatic { descriptor, .. } => { + let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?; + pop_slots(state, slots.len(), class, method, "putstatic")?; + Ok(()) + } + Insn::GetField { descriptor, .. } => { + pop_slots(state, 1, class, method, "objectref")?; + let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?; + push(state, slots) + } + Insn::PutField { descriptor, .. } => { + let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?; + pop_slots(state, slots.len(), class, method, "putfield")?; + pop_slots(state, 1, class, method, "objectref")?; + Ok(()) + } + Insn::Invoke { + kind, + owner, + name, + descriptor, + .. + } => invoke(class, method, state, *kind, owner, name, descriptor), + Insn::InvokeDynamic { descriptor, .. } => { + let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?; + pop_slots(state, count_slots(¶ms), class, method, "invokedynamic")?; + if let Some(ret) = ret { + push(state, ret)?; + } + Ok(()) + } + Insn::New(_) => push(state, vec![VerificationType::Uninitialized(offset)]), + Insn::NewArray(kind) => { + pop_slots(state, 1, class, method, "newarray")?; + let descriptor = match kind { + ArrayKind::Boolean => "[Z", + ArrayKind::Char => "[C", + ArrayKind::Float => "[F", + ArrayKind::Double => "[D", + ArrayKind::Byte => "[B", + ArrayKind::Short => "[S", + ArrayKind::Int => "[I", + ArrayKind::Long => "[J", + ArrayKind::Reference => { + return Err(invalid(class, method, "newarray ссылочного типа")); + } + }; + push( + state, + vec![VerificationType::Object(descriptor.to_string())], + ) + } + Insn::ANewArray(name) => { + pop_slots(state, 1, class, method, "anewarray")?; + push(state, vec![VerificationType::Object(format!("[L{name};"))]) + } + Insn::MultiANewArray { descriptor, dims } => { + pop_slots(state, *dims as usize, class, method, "multianewarray")?; + push(state, vec![VerificationType::Object(descriptor.clone())]) + } + Insn::ArrayLength => { + pop_slots(state, 1, class, method, "arraylength")?; + push(state, vec![VerificationType::Integer]) + } + Insn::AThrow => { + pop_slots(state, 1, class, method, "athrow")?; + state.stack.clear(); + Ok(()) + } + Insn::CheckCast(name) => { + pop_slots(state, 1, class, method, "checkcast")?; + push(state, vec![VerificationType::Object(name.clone())]) + } + Insn::InstanceOf(_) => { + pop_slots(state, 1, class, method, "instanceof")?; + push(state, vec![VerificationType::Integer]) + } + Insn::MonitorEnter | Insn::MonitorExit => { + pop_slots(state, 1, class, method, "monitor")?; + Ok(()) + } + } +} diff --git a/crates/noxis-classfile/src/format/frames/xfer/stack.rs b/crates/noxis-classfile/src/format/frames/xfer/stack.rs new file mode 100644 index 0000000..0481536 --- /dev/null +++ b/crates/noxis-classfile/src/format/frames/xfer/stack.rs @@ -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, + 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) -> 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, 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 { + 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 { + 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) + } +} diff --git a/crates/noxis-classfile/src/format/mod.rs b/crates/noxis-classfile/src/format/mod.rs new file mode 100644 index 0000000..36826d1 --- /dev/null +++ b/crates/noxis-classfile/src/format/mod.rs @@ -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 { + parse::class::parse_class(bytes) +} + +/// Записать [`ClassFile`] в байты, включая пересчёт `Code`/`StackMapTable`. +pub fn write(class: &ClassFile) -> Result, 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() +} diff --git a/crates/noxis-classfile/src/format/parse/attrs.rs b/crates/noxis-classfile/src/format/parse/attrs.rs new file mode 100644 index 0000000..af4aec6 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/attrs.rs @@ -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 { + 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::>()?, + ), + RtAttr::InnerClasses { classes, .. } => Attribute::InnerClasses( + classes + .iter() + .map(|class| inner(pool, class)) + .collect::>()?, + ), + 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::>()?, + ), + RtAttr::BootstrapMethods { methods, .. } => Attribute::BootstrapMethods( + methods + .iter() + .map(|method| bootstrap(pool, method)) + .collect::>()?, + ), + 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::>()?, + }) + }) + .collect::>()?, + ), + RtAttr::Code { .. } | RtAttr::StackMapTable { .. } => { + return Err(ClassfileError::Parse( + "Code/StackMapTable разбирает вызывающий код метода".into(), + )); + } + other => raw(other)?, + }) +} + +/// Несмоделированный атрибут → `Raw`: имя + байты `info` (без заголовка). +pub(super) fn raw(attr: &RtAttr) -> Result { + 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 { + 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, 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 { + 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 { + 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::>()?, + }) +} + +/// Индекс пула, где `0` означает отсутствие значения. +fn optional_class(pool: &ConstantPool<'_>, index: u16) -> Result, ClassfileError> { + if index == 0 { + Ok(None) + } else { + Ok(Some(class_name(pool, index)?)) + } +} + +/// Индекс `CONSTANT_Utf8`, где `0` означает отсутствие значения. +fn optional_utf8(pool: &ConstantPool<'_>, index: u16) -> Result, ClassfileError> { + if index == 0 { + Ok(None) + } else { + Ok(Some(utf8(pool, index)?)) + } +} diff --git a/crates/noxis-classfile/src/format/parse/class.rs b/crates/noxis-classfile/src/format/parse/class.rs new file mode 100644 index 0000000..8a629e7 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/class.rs @@ -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 { + 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::>()?; + 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::>()?, + methods: rt + .methods + .iter() + .map(|method| convert_method(method, pool)) + .collect::>()?, + attributes: convert_attrs(&rt.attributes, pool)?, + }) +} + +fn convert_field(field: &RtField, pool: &ConstantPool<'_>) -> Result { + 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 { + let mut code: Option = None; + let mut rest: Vec = 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, ClassfileError> { + attrs + .iter() + .map(|attr| attrs::convert(attr, pool)) + .collect() +} + +/// Индекс суперкласса, где `0` — его отсутствие (`java/lang/Object`). +fn optional_class(pool: &ConstantPool<'_>, index: u16) -> Result, ClassfileError> { + if index == 0 { + Ok(None) + } else { + Ok(Some(class_name(pool, index)?)) + } +} diff --git a/crates/noxis-classfile/src/format/parse/code/frames.rs b/crates/noxis-classfile/src/format/parse/code/frames.rs new file mode 100644 index 0000000..09a6ab3 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/code/frames.rs @@ -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, ClassfileError> { + frames.iter().map(|frame| to_frame(frame, pool)).collect() +} + +fn to_frame(frame: &StackFrame, pool: &ConstantPool<'_>) -> Result { + 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 { + 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, ClassfileError> { + values.iter().map(|value| vtype(value, pool)).collect() +} + +/// Тип верификации backend'а → наш тип (имя класса берётся из constant pool). +fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result { + 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, ClassfileError> { + if index == 0 { + Ok(None) + } else { + Ok(Some(class_name(pool, index)?)) + } +} diff --git a/crates/noxis-classfile/src/format/parse/code/labels.rs b/crates/noxis-classfile/src/format/parse/code/labels.rs new file mode 100644 index 0000000..0352a8b --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/code/labels.rs @@ -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, +) -> Result, 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 { + 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 { + std::iter::once(value) +} + +/// Превратить отсортированный набор номеров в последовательные метки. +pub(super) fn assign(indexes: &BTreeSet) -> BTreeMap { + indexes + .iter() + .enumerate() + .map(|(position, index)| (*index, position as u32)) + .collect() +} + +/// Метка для номера инструкции. +pub(super) fn lookup(labels: &BTreeMap, index: u16) -> Result { + 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, +) -> 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 { + 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 { + 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, + starts: &[u16], + byte: u16, +) -> Result { + 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, + starts: &[u16], + start: u16, + length: u16, + code_length: u16, +) -> Result { + 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::try_from(position) + .map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into())) +} + +fn to_index_checked(index: i32) -> Result { + u32::try_from(index) + .map_err(|_| ClassfileError::Parse(format!("номер инструкции {index} вне тела метода"))) +} diff --git a/crates/noxis-classfile/src/format/parse/code/mod.rs b/crates/noxis-classfile/src/format/parse/code/mod.rs new file mode 100644 index 0000000..c6c4ebd --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/code/mod.rs @@ -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 { + 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::>()?; + + 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::>()?, + )), + 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::>()?, + )) + } + 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::>()?, + )); + } + 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), 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>) -> Result { + u16::try_from(cursor.position()) + .map_err(|_| ClassfileError::Parse("тело метода длиннее 64 КБ".into())) +} diff --git a/crates/noxis-classfile/src/format/parse/insns/basic.rs b/crates/noxis-classfile/src/format/parse/insns/basic.rs new file mode 100644 index 0000000..f1f7025 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/insns/basic.rs @@ -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, 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 } +} diff --git a/crates/noxis-classfile/src/format/parse/insns/flow.rs b/crates/noxis-classfile/src/format/parse/insns/flow.rs new file mode 100644 index 0000000..cfb3249 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/insns/flow.rs @@ -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, ClassfileError> { + let rel = |delta: i32| -> Result { 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 { + Ok(Insn::Branch(cond, resolve(target)?)) +} + +/// Цель `switch` — номер инструкции: база плюс относительная дельта backend'а. +fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result { + 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::try_from(target) + .map_err(|_| ClassfileError::Parse("широкий переход вне диапазона u16".into())) +} + +fn switch_table( + table: &TableSwitch, + rel: &dyn Fn(i32) -> Result, +) -> Result { + Ok(Insn::TableSwitch { + default: rel(table.default)?, + low: table.low, + high: table.high, + targets: table + .offsets + .iter() + .map(|offset| rel(*offset)) + .collect::>()?, + }) +} + +fn switch_lookup( + lookup: &LookupSwitch, + rel: &dyn Fn(i32) -> Result, +) -> Result { + Ok(Insn::LookupSwitch { + default: rel(lookup.default)?, + pairs: lookup + .pairs + .iter() + .map(|(key, offset)| Ok((*key, rel(*offset)?))) + .collect::>()?, + }) +} diff --git a/crates/noxis-classfile/src/format/parse/insns/mod.rs b/crates/noxis-classfile/src/format/parse/insns/mod.rs new file mode 100644 index 0000000..93e9efc --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/insns/mod.rs @@ -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 + 'a; + +/// Конвертировать одну инструкцию. `index` — её номер в теле метода (нужно `switch`). +pub(super) fn convert( + instruction: &Instruction, + index: u16, + pool: &ConstantPool<'_>, + resolve: &Resolver<'_>, +) -> Result { + 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:?}" + ))), + } +} diff --git a/crates/noxis-classfile/src/format/parse/insns/refs.rs b/crates/noxis-classfile/src/format/parse/insns/refs.rs new file mode 100644 index 0000000..de2dc67 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/insns/refs.rs @@ -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, 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 { + 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 { + 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, + } +} diff --git a/crates/noxis-classfile/src/format/parse/mod.rs b/crates/noxis-classfile/src/format/parse/mod.rs new file mode 100644 index 0000000..d103e9b --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/mod.rs @@ -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; diff --git a/crates/noxis-classfile/src/format/parse/pool.rs b/crates/noxis-classfile/src/format/parse/pool.rs new file mode 100644 index 0000000..dd8c538 --- /dev/null +++ b/crates/noxis-classfile/src/format/parse/pool.rs @@ -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 { + 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 { + 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 { + 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:?} указывает на несовместимую ссылку" + ))) + } +} diff --git a/crates/noxis-classfile/src/format/write/attrs.rs b/crates/noxis-classfile/src/format/write/attrs.rs new file mode 100644 index 0000000..cd64694 --- /dev/null +++ b/crates/noxis-classfile/src/format/write/attrs.rs @@ -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, + pub byte_of: &'a dyn Fn(u32) -> Result, +} + +/// `LineNumberTable`: backend хранит номера инструкций и маппит сам. +pub(super) fn line_number_table( + lines: &[LineNumber], + pool: &mut Pool, + positions: &Positions<'_>, +) -> Result { + 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::>()?, + }) +} + +/// `LocalVariableTable`/`TypeTable` с пересчётом диапазона. +pub(super) fn local_table( + vars: &[LocalVariable], + pool: &mut Pool, + positions: &Positions<'_>, +) -> Result { + 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::>()?, + }) +} +/// Та же таблица для типов (`signature` вместо дескриптора). +pub(super) fn local_type_table( + vars: &[LocalVariableType], + pool: &mut Pool, + positions: &Positions<'_>, +) -> Result { + 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::>()?, + }) +} + +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 { + 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::>()?, + }, + 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::>()?, + }, + 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::>()?, + }, + 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::>()?, + }, + 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::>()?, + }) + }) + .collect::>()?, + }, + }) +} + +fn inner(class: &InnerClass, pool: &mut Pool) -> Result { + 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 { + 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::>()?, + }) +} + +/// Аргумент bootstrap-метода: те же константы, что в `ldc`, плюс `Dynamic`. +fn bootstrap_arg(arg: &Const, pool: &mut Pool) -> Result { + match arg { + Const::Dynamic { + name, + descriptor, + bootstrap, + } => pool.dynamic(*bootstrap, name, descriptor), + _ => pool.constant(arg), + } +} + +fn method_param(param: &MethodParameter, pool: &mut Pool) -> Result { + Ok(RtParam { + name_index: match ¶m.name { + None => 0, + Some(name) => pool.utf8(name)?, + }, + access_flags: MethodAccessFlags::from_bits_truncate(param.access_flags), + }) +} diff --git a/crates/noxis-classfile/src/format/write/code/frames.rs b/crates/noxis-classfile/src/format/write/code/frames.rs new file mode 100644 index 0000000..9f23677 --- /dev/null +++ b/crates/noxis-classfile/src/format/write/code/frames.rs @@ -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, ClassfileError> { + frames + .iter() + .map(|frame| build_frame(frame, pool)) + .collect() +} + +fn build_frame(frame: &StackMapFrame, pool: &mut Pool) -> Result { + 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, ClassfileError> { + values.iter().map(|value| vtype(value, pool)).collect() +} + +fn vtype(value: &VerificationType, pool: &mut Pool) -> Result { + 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 }, + }) +} diff --git a/crates/noxis-classfile/src/format/write/code/insns.rs b/crates/noxis-classfile/src/format/write/code/insns.rs new file mode 100644 index 0000000..031682a --- /dev/null +++ b/crates/noxis-classfile/src/format/write/code/insns.rs @@ -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, +) -> Result, 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::>()?, + })), + 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::, 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 { + 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)) +} diff --git a/crates/noxis-classfile/src/format/write/code/mod.rs b/crates/noxis-classfile/src/format/write/code/mod.rs new file mode 100644 index 0000000..0ecd58b --- /dev/null +++ b/crates/noxis-classfile/src/format/write/code/mod.rs @@ -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 { + let (labels, bare) = split_marks(&code.insns)?; + let resolve = |label: Label| -> Result { + 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::>()?; + let to_byte = measure(&rt_insns)?; + let byte_of = |index: u32| -> Result { + 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, 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, 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>) -> Result { + u16::try_from(cursor.position()) + .map_err(|_| ClassfileError::Write("тело метода длиннее 64 КБ".into())) +} + +fn entry( + handler: &ExceptionHandler, + resolve: &impl Fn(Label) -> Result, + pool: &mut Pool, +) -> Result { + 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 { + 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) -> 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::try_from(index) + .map_err(|_| ClassfileError::Write(format!("номер инструкции {index} вне u16"))) +} + +/// Дельта `switch`: цель минус позиция самого `switch`. +fn delta(index: u32, target: u32) -> Result { + 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::try_from(value) + .map_err(|_| ClassfileError::Write(format!("номер инструкции {value} вне i32"))) +} diff --git a/crates/noxis-classfile/src/format/write/code/ops.rs b/crates/noxis-classfile/src/format/write/code/ops.rs new file mode 100644 index 0000000..ef57be7 --- /dev/null +++ b/crates/noxis-classfile/src/format/write/code/ops.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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}")) +} diff --git a/crates/noxis-classfile/src/format/write/mod.rs b/crates/noxis-classfile/src/format/write/mod.rs new file mode 100644 index 0000000..02813e6 --- /dev/null +++ b/crates/noxis-classfile/src/format/write/mod.rs @@ -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, 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 { + 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::>()?, + }) +} + +fn convert_method(method: &Method, pool: &mut pool::Pool) -> Result { + 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, + }) +} diff --git a/crates/noxis-classfile/src/format/write/pool.rs b/crates/noxis-classfile/src/format/write/pool.rs new file mode 100644 index 0000000..cee7efc --- /dev/null +++ b/crates/noxis-classfile/src/format/write/pool.rs @@ -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 { + self.pool.add_utf8(value).map_err(write_err) + } + + pub(super) fn name_and_type( + &mut self, + name: &str, + descriptor: &str, + ) -> Result { + self.pool + .add_name_and_type(name, descriptor) + .map_err(write_err) + } + + pub(super) fn class(&mut self, name: &str) -> Result { + self.pool.add_class(name).map_err(write_err) + } + + pub(super) fn field_ref( + &mut self, + owner: &str, + name: &str, + descriptor: &str, + ) -> Result { + 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 { + 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 { + 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 { + 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 { + 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 { + 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()) +} diff --git a/crates/noxis-classfile/src/insn/kinds.rs b/crates/noxis-classfile/src/insn/kinds.rs new file mode 100644 index 0000000..dc340b5 --- /dev/null +++ b/crates/noxis-classfile/src/insn/kinds.rs @@ -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` относительно нуля. + Zero(Cond), + /// `if_icmp`. + Int(Cond), + /// `if_acmp`. + 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). +#[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, +} diff --git a/crates/noxis-classfile/src/insn/mod.rs b/crates/noxis-classfile/src/insn/mod.rs new file mode 100644 index 0000000..0a4facc --- /dev/null +++ b/crates/noxis-classfile/src/insn/mod.rs @@ -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