initial working implementation

This commit is contained in:
raf 2025-05-24 23:27:15 +03:00
commit c4beb3e65f
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
8 changed files with 3858 additions and 126 deletions

View file

@ -1,47 +1,33 @@
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
mod ast;
mod cst;
mod lexer;
mod parser;
mod syntax;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use clap::Parser;
use anyhow::{Context, Result};
use thiserror::Error;
use crate::lexer::NftablesLexer;
use crate::parser::Parser as NftablesParser;
use crate::syntax::{FormatConfig, IndentStyle, NftablesFormatter};
use crate::cst::CstBuilder;
#[derive(Error, Debug)]
enum FormatterError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Invalid file: {0}")]
InvalidFile(String),
#[error("Parse error: {0}")]
ParseError(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",
@ -69,87 +55,14 @@ struct Args {
/// 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,
}
/// Show debug information (tokens, AST, etc.)
#[arg(long)]
debug: 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
}
/// Check syntax only, don't format
#[arg(long)]
check: bool,
}
fn process_nftables_config(args: Args) -> Result<()> {
@ -162,33 +75,92 @@ fn process_nftables_config(args: Args) -> Result<()> {
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))?;
// Read file contents
let source = fs::read_to_string(&args.file)
.with_context(|| format!("Failed to read 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)
// Tokenize
let mut lexer = NftablesLexer::new(&source);
let tokens = if args.debug {
// Use error-recovery tokenization for debug mode
lexer.tokenize_with_errors()
} else {
output_content
lexer.tokenize()
.map_err(|e| FormatterError::ParseError(e.to_string()))?
};
if args.debug {
eprintln!("=== TOKENS ===");
for (i, token) in tokens.iter().enumerate() {
eprintln!("{:3}: {:?} @ {:?} = '{}'", i, token.kind, token.range, token.text);
}
eprintln!();
// Build and validate CST
eprintln!("=== CST ===");
let cst_tree = CstBuilder::build_tree(&tokens);
match CstBuilder::validate_tree(&cst_tree) {
Ok(()) => eprintln!("CST validation passed"),
Err(e) => eprintln!("CST validation error: {}", e),
}
// Also test parse_to_cst
match CstBuilder::parse_to_cst(&tokens) {
Ok(_) => eprintln!("CST parsing successful"),
Err(e) => eprintln!("CST parsing error: {}", e),
}
eprintln!();
}
// Parse
let ruleset = if args.debug {
// Use error-recovery parsing for debug mode
let (parsed_ruleset, errors) = NftablesParser::parse_with_errors(&source);
if !errors.is_empty() {
eprintln!("=== PARSE ERRORS ===");
for error in &errors {
eprintln!("Parse error: {}", error);
}
eprintln!();
}
parsed_ruleset.unwrap_or_else(|| crate::ast::Ruleset::new())
} else {
let mut parser = NftablesParser::new(tokens.clone());
parser.parse()
.map_err(|e| FormatterError::ParseError(e.to_string()))?
};
if args.debug {
eprintln!("=== AST ===");
eprintln!("{:#?}", ruleset);
eprintln!();
}
if args.check {
println!("Syntax check passed for: {}", args.file);
return Ok(());
}
// Format
let config = FormatConfig {
indent_style: args.indent,
spaces_per_level: args.spaces,
optimize: args.optimize,
max_empty_lines: if args.optimize { 1 } else { 2 },
};
let formatter = NftablesFormatter::new(config);
let formatted_output = formatter.format_ruleset(&ruleset);
// Write output
match &args.output {
Some(output_file) => {
fs::write(output_file, output_content)
fs::write(output_file, &formatted_output)
.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())
io::stdout().write_all(formatted_output.as_bytes())
.with_context(|| "Failed to write to stdout")?;
}
}
@ -198,5 +170,19 @@ fn process_nftables_config(args: Args) -> Result<()> {
fn main() -> Result<()> {
let args = Args::parse();
process_nftables_config(args)
if let Err(e) = process_nftables_config(args) {
eprintln!("Error: {}", e);
// Print the error chain
let mut current = e.source();
while let Some(cause) = current {
eprintln!(" Caused by: {}", cause);
current = cause.source();
}
std::process::exit(1);
}
Ok(())
}