From f4cb9fe9a1366774ace8075a3ab97d5e90508381 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 24 May 2025 16:03:32 +0300 Subject: [PATCH] initial commit --- .gitignore | 5 ++ Cargo.lock | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 +++ src/main.rs | 202 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 469 insertions(+) create mode 100644 .gitignore create mode 100755 Cargo.lock create mode 100755 Cargo.toml create mode 100755 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..738c972 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Ignore build artifacts +target* + +# Ignore test files +*.nft diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..94021ad --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..b7cbf7a --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..2704820 --- /dev/null +++ b/src/main.rs @@ -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 { + 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, + + /// 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) -> Vec { + 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, 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) +}