Initial commit: noxis JVM bytecode obfuscator

Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis,
noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check).
AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

View file

@ -0,0 +1,79 @@
//! Compile-time gate for the project structure rules.
//!
//! Every crate calls [`check`] from its `build.rs`, so a file that grows past
//! 250 lines or a directory that fills up with a fifth file fails the build.
mod rules;
pub use rules::{MAX_DIR_FILES, MAX_FILE_LINES, check, scan};
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::fs;
use std::path::{Path, PathBuf};
use super::scan;
struct TempDir(PathBuf);
impl TempDir {
fn new(tag: &str) -> Self {
let path =
std::env::temp_dir().join(format!("noxis-structure-{tag}-{}", std::process::id()));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).expect("create temp dir");
Self(path)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn violations(dir: &Path) -> Vec<String> {
let mut out = Vec::new();
scan(dir, &mut out);
out
}
#[test]
fn rejects_file_over_line_limit() {
let tmp = TempDir::new("long");
fs::write(tmp.0.join("lib.rs"), "fn main() {}\n".repeat(300)).expect("write");
let found = violations(&tmp.0);
assert_eq!(found.len(), 1, "{found:?}");
assert!(found[0].contains("lines >"), "{found:?}");
}
#[test]
fn rejects_directory_over_file_limit() {
let tmp = TempDir::new("many");
for index in 0..super::MAX_DIR_FILES + 1 {
fs::write(tmp.0.join(format!("m{index}.rs")), "").expect("write");
}
let found = violations(&tmp.0);
assert_eq!(found.len(), 1, "{found:?}");
assert!(found[0].contains("files >"), "{found:?}");
}
#[test]
fn accepts_compact_tree() {
let tmp = TempDir::new("ok");
fs::create_dir(tmp.0.join("sub")).expect("mkdir");
fs::write(tmp.0.join("lib.rs"), "pub fn f() {}\n").expect("write");
fs::write(tmp.0.join("sub").join("a.rs"), "pub fn a() {}\n").expect("write");
let found = violations(&tmp.0);
assert!(found.is_empty(), "{found:?}");
}
#[test]
fn ignores_non_limited_extensions() {
let tmp = TempDir::new("ext");
fs::write(tmp.0.join("notes.txt"), "line\n".repeat(300)).expect("write");
let found = violations(&tmp.0);
assert!(found.is_empty(), "{found:?}");
}
}

View file

@ -0,0 +1,165 @@
//! Project structure rules, invoked from every crate's `build.rs`.
//!
//! Rules:
//! * every `.rs` and `.md` file is at most [`MAX_FILE_LINES`] lines long;
//! * every directory inside a crate `src/` or the workspace `docs/` holds at
//! most [`MAX_DIR_FILES`] files — subdirectories are unlimited.
//!
//! Deliberately excluded: `Cargo.lock`, `.gitignore`, the legacy `info/` tree,
//! and crate roots (they always own `Cargo.toml` plus `build.rs`).
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
/// Maximum number of lines in a source (`rs`) or documentation (`md`) file.
pub const MAX_FILE_LINES: usize = 250;
/// Maximum number of files per directory; subdirectories are not counted.
pub const MAX_DIR_FILES: usize = 4;
/// Extensions subject to [`MAX_FILE_LINES`].
const LINE_LIMITED_EXTS: [&str; 2] = ["rs", "md"];
/// Directory names the scan never descends into.
const SKIPPED_DIRS: [&str; 2] = ["target", ".git"];
/// Scans the calling crate's `src/`/`tests/`/`benches/` plus the workspace
/// `docs/` tree and panics with a report on the first violation.
///
/// # Panics
///
/// Panics whenever a rule is violated — that is the point of the gate.
pub fn check() {
let manifest = manifest_dir();
let mut violations = Vec::new();
// `src/` обязателен; `tests/` и `benches/` проверяются, если созданы —
// иначе правило «250 строк» обходится выносом кода в тесты.
let mut roots = vec![manifest.join("src"), manifest.join("tests")];
roots.push(manifest.join("benches"));
if let Some(root) = workspace_root(&manifest) {
roots.push(root.join("docs"));
}
for root in &roots {
watch(root);
scan(root, &mut violations);
}
if violations.is_empty() {
return;
}
let mut report = String::from("project structure rules violated:\n");
for violation in &violations {
report.push_str(" - ");
report.push_str(violation);
report.push('\n');
}
report.push_str("split the file, or move code into a subdirectory (rules: docs/README.md)");
panic!("{report}");
}
/// Re-runs the build script whenever a checked tree changes.
///
/// Without this cargo only invalidates `build.rs` on crate-local changes, so
/// editing `docs/` would never be re-validated.
fn watch(root: &Path) {
if root.is_dir() {
println!("cargo:rerun-if-changed={}", root.display());
}
}
/// Collects every rule violation found under `root` into `violations`.
pub fn scan(root: &Path, violations: &mut Vec<String>) {
if !root.is_dir() {
return;
}
let entries = match fs::read_dir(root) {
Ok(entries) => entries,
Err(err) => {
violations.push(format!("{}: unreadable: {err}", root.display()));
return;
}
};
let mut files = 0usize;
let mut subdirs = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = entry.file_name();
if SKIPPED_DIRS
.iter()
.any(|s| name.as_os_str() == OsStr::new(s))
{
continue;
}
subdirs.push(path);
continue;
}
files += 1;
check_line_limit(&path, violations);
}
if files > MAX_DIR_FILES {
violations.push(format!(
"{}: {files} files > {MAX_DIR_FILES} (subdirectories are allowed)",
root.display()
));
}
for subdir in subdirs {
scan(&subdir, violations);
}
}
fn check_line_limit(path: &Path, violations: &mut Vec<String>) {
let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default();
if !LINE_LIMITED_EXTS.contains(&ext) {
return;
}
let lines = count_lines(path);
if lines > MAX_FILE_LINES {
violations.push(format!(
"{}: {lines} lines > {MAX_FILE_LINES}",
path.display()
));
}
}
/// Counts lines without requiring the file to be valid UTF-8.
fn count_lines(path: &Path) -> usize {
let Ok(bytes) = fs::read(path) else {
return 0;
};
if bytes.is_empty() {
return 0;
}
let newlines = bytes.iter().filter(|&&b| b == b'\n').count();
newlines + usize::from(bytes.last() != Some(&b'\n'))
}
fn manifest_dir() -> PathBuf {
match std::env::var_os("CARGO_MANIFEST_DIR") {
Some(value) => PathBuf::from(value),
None => panic!("CARGO_MANIFEST_DIR is unset; run this check from a build script"),
}
}
/// Walks up from `start` until a manifest declaring `[workspace]` is found.
fn workspace_root(start: &Path) -> Option<PathBuf> {
let mut current = Some(start);
while let Some(dir) = current {
if let Ok(text) = fs::read_to_string(dir.join("Cargo.toml"))
&& text.lines().any(|line| line.trim() == "[workspace]")
{
return Some(dir.to_path_buf());
}
current = dir.parent();
}
None
}