Initial commit: noxis JVM bytecode obfuscator
Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis, noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check). AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
13
crates/noxis-structure-check/Cargo.toml
Normal file
13
crates/noxis-structure-check/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "noxis-structure-check"
|
||||
description = "Автогейт структурных правил проекта при компиляции"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
crates/noxis-structure-check/build.rs
Normal file
9
crates/noxis-structure-check/build.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
//! Bootstrap gate: this crate cannot depend on itself, so it includes the
|
||||
//! shared rules source directly instead of going through the crate API.
|
||||
|
||||
#[path = "src/rules.rs"]
|
||||
mod rules;
|
||||
|
||||
fn main() {
|
||||
rules::check();
|
||||
}
|
||||
79
crates/noxis-structure-check/src/lib.rs
Normal file
79
crates/noxis-structure-check/src/lib.rs
Normal 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:?}");
|
||||
}
|
||||
}
|
||||
165
crates/noxis-structure-check/src/rules.rs
Normal file
165
crates/noxis-structure-check/src/rules.rs
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue