initial commit
This commit is contained in:
commit
f4cb9fe9a1
4 changed files with 469 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Ignore build artifacts
|
||||||
|
target*
|
||||||
|
|
||||||
|
# Ignore test files
|
||||||
|
*.nft
|
251
Cargo.lock
generated
Executable file
251
Cargo.lock
generated
Executable 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
11
Cargo.toml
Executable 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
202
src/main.rs
Executable 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)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue