initial commit

This commit is contained in:
raf 2025-05-24 16:03:32 +03:00
commit f4cb9fe9a1
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 469 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
# Ignore build artifacts
target*
# Ignore test files
*.nft

251
Cargo.lock generated Executable file
View file

@ -0,0 +1,251 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "clap"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "nff"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"thiserror",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
[[package]]
name = "windows_i686_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
[[package]]
name = "windows_i686_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"

11
Cargo.toml Executable file
View file

@ -0,0 +1,11 @@
[package]
name = "nff"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5", features = ["derive"] }
anyhow = "1.0"
thiserror = "2.0"

202
src/main.rs Executable file
View file

@ -0,0 +1,202 @@
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
use std::path::Path;
use clap::Parser;
use anyhow::{Context, Result};
use thiserror::Error;
#[derive(Error, Debug)]
enum FormatterError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Invalid file: {0}")]
InvalidFile(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
}
#[derive(Debug, Clone, Copy)]
enum IndentStyle {
Tabs,
Spaces,
}
impl IndentStyle {
fn format(&self, level: usize, spaces_per_level: usize) -> String {
match self {
Self::Tabs => "\t".repeat(level),
Self::Spaces => " ".repeat(spaces_per_level * level),
}
}
}
impl std::str::FromStr for IndentStyle {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"tabs" | "tab" => Ok(Self::Tabs),
"spaces" | "space" => Ok(Self::Spaces),
_ => Err(format!("Invalid indent style: {}. Use 'tabs' or 'spaces'", s)),
}
}
}
#[derive(Parser, Debug)]
#[command(
name = "nff",
version = "0.1.0",
about = "A high-quality nftables formatter and beautifier",
long_about = "nff (nftables formatter) is a tool for formatting and beautifying nftables configuration files with proper indentation and structure."
)]
struct Args {
/// nftables config file (e.g: /etc/nftables.conf)
#[arg(short, long, value_name = "FILE")]
file: String,
/// Type of indentation
#[arg(short, long, default_value = "tabs", value_parser = clap::value_parser!(IndentStyle))]
indent: IndentStyle,
/// Output file (writes to stdout if not specified)
#[arg(short, long, value_name = "FILE")]
output: Option<String>,
/// Optimize output by removing excessive empty lines
#[arg(long)]
optimize: bool,
/// Number of spaces per indentation level (only used with --indent=spaces)
#[arg(long, default_value = "2", value_name = "N")]
spaces: usize,
}
struct NftablesFormatter {
indent_style: IndentStyle,
spaces_per_level: usize,
optimize: bool,
}
impl NftablesFormatter {
fn new(indent_style: IndentStyle, spaces_per_level: usize, optimize: bool) -> Self {
Self {
indent_style,
spaces_per_level,
optimize,
}
}
fn format_lines(&self, lines: Vec<String>) -> Vec<String> {
let mut output_lines = Vec::new();
let mut level = 0;
let mut prev_was_empty = false;
for (i, line) in lines.iter().enumerate() {
let line = line.trim();
// Handle empty lines
if line.is_empty() {
if self.optimize {
if prev_was_empty {
continue;
}
prev_was_empty = true;
} else {
prev_was_empty = false;
}
output_lines.push(String::new());
continue;
} else {
prev_was_empty = false;
}
// Skip lines that contain both opening and closing braces (single-line blocks)
if line.contains('{') && line.contains('}') {
continue;
}
// Adjust indentation level before formatting if this line closes a block
if line.ends_with('}') || line == "}" {
if level > 0 {
level -= 1;
}
}
// Generate indentation
let indentation = self.indent_style.format(level, self.spaces_per_level);
// Format the line
let formatted_line = format!("{}{}", indentation, line);
// Skip empty lines before closing braces if optimizing
if self.optimize && i > 0 && lines[i-1].trim().is_empty() {
if line.ends_with('}') || line == "}" {
// Remove the last empty line
if let Some(last) = output_lines.last() {
if last.trim().is_empty() {
output_lines.pop();
}
}
}
}
output_lines.push(formatted_line);
// Adjust indentation level after formatting if this line opens a block
if line.ends_with('{') {
level += 1;
}
}
output_lines
}
}
fn process_nftables_config(args: Args) -> Result<()> {
let path = Path::new(&args.file);
if !path.exists() {
return Err(FormatterError::FileNotFound(args.file).into());
}
if !path.is_file() {
return Err(FormatterError::InvalidFile("Not a regular file".to_string()).into());
}
let file = File::open(&args.file)
.with_context(|| format!("Failed to open file: {}", args.file))?;
let reader = BufReader::new(file);
let lines: Result<Vec<String>, io::Error> = reader.lines().collect();
let lines = lines.with_context(|| "Failed to read file contents")?;
let formatter = NftablesFormatter::new(args.indent, args.spaces, args.optimize);
let formatted_lines = formatter.format_lines(lines);
// Create output content
let output_content = formatted_lines.join("\n");
let output_content = if !output_content.ends_with('\n') && !output_content.is_empty() {
format!("{}\n", output_content)
} else {
output_content
};
// Write output
match &args.output {
Some(output_file) => {
fs::write(output_file, output_content)
.with_context(|| format!("Failed to write to output file: {}", output_file))?;
println!("Formatted output written to: {}", output_file);
}
None => {
io::stdout().write_all(output_content.as_bytes())
.with_context(|| "Failed to write to stdout")?;
}
}
Ok(())
}
fn main() -> Result<()> {
let args = Args::parse();
process_nftables_config(args)
}