initial working implementation
This commit is contained in:
parent
b865832523
commit
c4beb3e65f
8 changed files with 3858 additions and 126 deletions
236
src/main.rs
236
src/main.rs
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue