treewide: fmt
This commit is contained in:
parent
c4beb3e65f
commit
10a525b2e7
5 changed files with 326 additions and 206 deletions
119
src/cst.rs
119
src/cst.rs
|
@ -3,14 +3,10 @@
|
||||||
//! Lossless representation preserving whitespace, comments, and formatting.
|
//! Lossless representation preserving whitespace, comments, and formatting.
|
||||||
|
|
||||||
use crate::lexer::{Token, TokenKind};
|
use crate::lexer::{Token, TokenKind};
|
||||||
use cstree::{
|
use cstree::{RawSyntaxKind, green::GreenNode, util::NodeOrToken};
|
||||||
green::GreenNode,
|
|
||||||
RawSyntaxKind, util::NodeOrToken,
|
|
||||||
};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
||||||
/// nftables syntax node types
|
/// nftables syntax node types
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
|
@ -350,7 +346,10 @@ impl CstBuilder {
|
||||||
|
|
||||||
/// Internal tree builder that constructs CST according to nftables grammar
|
/// Internal tree builder that constructs CST according to nftables grammar
|
||||||
struct CstTreeBuilder {
|
struct CstTreeBuilder {
|
||||||
stack: Vec<(SyntaxKind, Vec<NodeOrToken<GreenNode, cstree::green::GreenToken>>)>,
|
stack: Vec<(
|
||||||
|
SyntaxKind,
|
||||||
|
Vec<NodeOrToken<GreenNode, cstree::green::GreenToken>>,
|
||||||
|
)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CstTreeBuilder {
|
impl CstTreeBuilder {
|
||||||
|
@ -384,15 +383,15 @@ impl CstTreeBuilder {
|
||||||
TokenKind::CommentLine(_) => {
|
TokenKind::CommentLine(_) => {
|
||||||
self.add_token(&tokens[start], SyntaxKind::Comment);
|
self.add_token(&tokens[start], SyntaxKind::Comment);
|
||||||
start + 1
|
start + 1
|
||||||
},
|
}
|
||||||
TokenKind::Newline => {
|
TokenKind::Newline => {
|
||||||
self.add_token(&tokens[start], SyntaxKind::Newline);
|
self.add_token(&tokens[start], SyntaxKind::Newline);
|
||||||
start + 1
|
start + 1
|
||||||
},
|
}
|
||||||
TokenKind::Shebang(_) => {
|
TokenKind::Shebang(_) => {
|
||||||
self.add_token(&tokens[start], SyntaxKind::Shebang);
|
self.add_token(&tokens[start], SyntaxKind::Shebang);
|
||||||
start + 1
|
start + 1
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Handle other tokens as statements
|
// Handle other tokens as statements
|
||||||
self.parse_statement(tokens, start)
|
self.parse_statement(tokens, start)
|
||||||
|
@ -437,17 +436,17 @@ impl CstTreeBuilder {
|
||||||
match &tokens[pos].kind {
|
match &tokens[pos].kind {
|
||||||
TokenKind::Chain => {
|
TokenKind::Chain => {
|
||||||
pos = self.parse_chain(tokens, pos);
|
pos = self.parse_chain(tokens, pos);
|
||||||
},
|
}
|
||||||
TokenKind::Set => {
|
TokenKind::Set => {
|
||||||
pos = self.parse_set(tokens, pos);
|
pos = self.parse_set(tokens, pos);
|
||||||
},
|
}
|
||||||
TokenKind::Map => {
|
TokenKind::Map => {
|
||||||
pos = self.parse_map(tokens, pos);
|
pos = self.parse_map(tokens, pos);
|
||||||
},
|
}
|
||||||
TokenKind::Newline | TokenKind::CommentLine(_) => {
|
TokenKind::Newline | TokenKind::CommentLine(_) => {
|
||||||
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
||||||
pos += 1;
|
pos += 1;
|
||||||
},
|
}
|
||||||
_ => pos += 1, // Skip unknown tokens
|
_ => pos += 1, // Skip unknown tokens
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -484,11 +483,11 @@ impl CstTreeBuilder {
|
||||||
match &tokens[pos].kind {
|
match &tokens[pos].kind {
|
||||||
TokenKind::Type => {
|
TokenKind::Type => {
|
||||||
pos = self.parse_chain_properties(tokens, pos);
|
pos = self.parse_chain_properties(tokens, pos);
|
||||||
},
|
}
|
||||||
TokenKind::Newline | TokenKind::CommentLine(_) => {
|
TokenKind::Newline | TokenKind::CommentLine(_) => {
|
||||||
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
||||||
pos += 1;
|
pos += 1;
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Parse as rule
|
// Parse as rule
|
||||||
pos = self.parse_rule(tokens, pos);
|
pos = self.parse_rule(tokens, pos);
|
||||||
|
@ -538,7 +537,9 @@ impl CstTreeBuilder {
|
||||||
TokenKind::Output => self.add_token(&tokens[pos], SyntaxKind::OutputKw),
|
TokenKind::Output => self.add_token(&tokens[pos], SyntaxKind::OutputKw),
|
||||||
TokenKind::Forward => self.add_token(&tokens[pos], SyntaxKind::ForwardKw),
|
TokenKind::Forward => self.add_token(&tokens[pos], SyntaxKind::ForwardKw),
|
||||||
TokenKind::Prerouting => self.add_token(&tokens[pos], SyntaxKind::PreroutingKw),
|
TokenKind::Prerouting => self.add_token(&tokens[pos], SyntaxKind::PreroutingKw),
|
||||||
TokenKind::Postrouting => self.add_token(&tokens[pos], SyntaxKind::PostroutingKw),
|
TokenKind::Postrouting => {
|
||||||
|
self.add_token(&tokens[pos], SyntaxKind::PostroutingKw)
|
||||||
|
}
|
||||||
_ => self.add_token(&tokens[pos], SyntaxKind::Identifier),
|
_ => self.add_token(&tokens[pos], SyntaxKind::Identifier),
|
||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
@ -580,7 +581,7 @@ impl CstTreeBuilder {
|
||||||
self.add_token(&tokens[pos], SyntaxKind::Newline);
|
self.add_token(&tokens[pos], SyntaxKind::Newline);
|
||||||
pos += 1;
|
pos += 1;
|
||||||
break;
|
break;
|
||||||
},
|
}
|
||||||
TokenKind::Accept => self.add_token(&tokens[pos], SyntaxKind::AcceptKw),
|
TokenKind::Accept => self.add_token(&tokens[pos], SyntaxKind::AcceptKw),
|
||||||
TokenKind::Drop => self.add_token(&tokens[pos], SyntaxKind::DropKw),
|
TokenKind::Drop => self.add_token(&tokens[pos], SyntaxKind::DropKw),
|
||||||
TokenKind::Reject => self.add_token(&tokens[pos], SyntaxKind::RejectKw),
|
TokenKind::Reject => self.add_token(&tokens[pos], SyntaxKind::RejectKw),
|
||||||
|
@ -596,10 +597,14 @@ impl CstTreeBuilder {
|
||||||
TokenKind::LeftParen => {
|
TokenKind::LeftParen => {
|
||||||
pos = self.parse_expression(tokens, pos);
|
pos = self.parse_expression(tokens, pos);
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
TokenKind::Identifier(_) => self.add_token(&tokens[pos], SyntaxKind::Identifier),
|
TokenKind::Identifier(_) => self.add_token(&tokens[pos], SyntaxKind::Identifier),
|
||||||
TokenKind::StringLiteral(_) => self.add_token(&tokens[pos], SyntaxKind::StringLiteral),
|
TokenKind::StringLiteral(_) => {
|
||||||
TokenKind::NumberLiteral(_) => self.add_token(&tokens[pos], SyntaxKind::NumberLiteral),
|
self.add_token(&tokens[pos], SyntaxKind::StringLiteral)
|
||||||
|
}
|
||||||
|
TokenKind::NumberLiteral(_) => {
|
||||||
|
self.add_token(&tokens[pos], SyntaxKind::NumberLiteral)
|
||||||
|
}
|
||||||
TokenKind::IpAddress(_) => self.add_token(&tokens[pos], SyntaxKind::IpAddress),
|
TokenKind::IpAddress(_) => self.add_token(&tokens[pos], SyntaxKind::IpAddress),
|
||||||
_ => self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone())),
|
_ => self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone())),
|
||||||
}
|
}
|
||||||
|
@ -627,7 +632,7 @@ impl CstTreeBuilder {
|
||||||
TokenKind::LeftParen => {
|
TokenKind::LeftParen => {
|
||||||
paren_depth += 1;
|
paren_depth += 1;
|
||||||
self.add_token(&tokens[pos], SyntaxKind::LeftParen);
|
self.add_token(&tokens[pos], SyntaxKind::LeftParen);
|
||||||
},
|
}
|
||||||
TokenKind::RightParen => {
|
TokenKind::RightParen => {
|
||||||
self.add_token(&tokens[pos], SyntaxKind::RightParen);
|
self.add_token(&tokens[pos], SyntaxKind::RightParen);
|
||||||
paren_depth -= 1;
|
paren_depth -= 1;
|
||||||
|
@ -635,20 +640,22 @@ impl CstTreeBuilder {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
TokenKind::LeftBracket => {
|
TokenKind::LeftBracket => {
|
||||||
pos = self.parse_set_expression(tokens, pos);
|
pos = self.parse_set_expression(tokens, pos);
|
||||||
continue;
|
continue;
|
||||||
},
|
}
|
||||||
TokenKind::Identifier(_) => {
|
TokenKind::Identifier(_) => {
|
||||||
// Check if this is a function call
|
// Check if this is a function call
|
||||||
if pos + 1 < tokens.len() && matches!(tokens[pos + 1].kind, TokenKind::LeftParen) {
|
if pos + 1 < tokens.len()
|
||||||
|
&& matches!(tokens[pos + 1].kind, TokenKind::LeftParen)
|
||||||
|
{
|
||||||
pos = self.parse_call_expression(tokens, pos);
|
pos = self.parse_call_expression(tokens, pos);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
self.add_token(&tokens[pos], SyntaxKind::Identifier);
|
self.add_token(&tokens[pos], SyntaxKind::Identifier);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
TokenKind::Dash => {
|
TokenKind::Dash => {
|
||||||
// Could be binary or unary expression
|
// Could be binary or unary expression
|
||||||
if self.is_binary_context() {
|
if self.is_binary_context() {
|
||||||
|
@ -656,7 +663,7 @@ impl CstTreeBuilder {
|
||||||
} else {
|
} else {
|
||||||
self.parse_unary_expression(tokens, pos);
|
self.parse_unary_expression(tokens, pos);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone())),
|
_ => self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone())),
|
||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
@ -789,7 +796,9 @@ impl CstTreeBuilder {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
// Parse what to flush (table, chain, etc.)
|
// Parse what to flush (table, chain, etc.)
|
||||||
while pos < tokens.len() && !matches!(tokens[pos].kind, TokenKind::Newline | TokenKind::Semicolon) {
|
while pos < tokens.len()
|
||||||
|
&& !matches!(tokens[pos].kind, TokenKind::Newline | TokenKind::Semicolon)
|
||||||
|
{
|
||||||
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
@ -845,7 +854,9 @@ impl CstTreeBuilder {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
// Parse element specification
|
// Parse element specification
|
||||||
while pos < tokens.len() && !matches!(tokens[pos].kind, TokenKind::Newline | TokenKind::LeftBrace) {
|
while pos < tokens.len()
|
||||||
|
&& !matches!(tokens[pos].kind, TokenKind::Newline | TokenKind::LeftBrace)
|
||||||
|
{
|
||||||
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
self.add_token(&tokens[pos], SyntaxKind::from(tokens[pos].kind.clone()));
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
@ -914,7 +925,9 @@ impl CstTreeBuilder {
|
||||||
|
|
||||||
fn is_binary_context(&self) -> bool {
|
fn is_binary_context(&self) -> bool {
|
||||||
// Simple heuristic: assume binary if we have recent tokens in current scope
|
// Simple heuristic: assume binary if we have recent tokens in current scope
|
||||||
self.stack.last().map_or(false, |(_, children)| children.len() > 1)
|
self.stack
|
||||||
|
.last()
|
||||||
|
.map_or(false, |(_, children)| children.len() > 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_node(&mut self, kind: SyntaxKind) {
|
fn start_node(&mut self, kind: SyntaxKind) {
|
||||||
|
@ -925,7 +938,8 @@ impl CstTreeBuilder {
|
||||||
// Handle whitespace specially if it's part of the token
|
// Handle whitespace specially if it's part of the token
|
||||||
if let TokenKind::Identifier(text) = &token.kind {
|
if let TokenKind::Identifier(text) = &token.kind {
|
||||||
if text.chars().all(|c| c.is_whitespace()) {
|
if text.chars().all(|c| c.is_whitespace()) {
|
||||||
let whitespace_token = GreenNode::new(SyntaxKind::Whitespace.to_raw(), std::iter::empty());
|
let whitespace_token =
|
||||||
|
GreenNode::new(SyntaxKind::Whitespace.to_raw(), std::iter::empty());
|
||||||
if let Some((_, children)) = self.stack.last_mut() {
|
if let Some((_, children)) = self.stack.last_mut() {
|
||||||
children.push(NodeOrToken::Node(whitespace_token));
|
children.push(NodeOrToken::Node(whitespace_token));
|
||||||
}
|
}
|
||||||
|
@ -978,14 +992,17 @@ impl CstValidator {
|
||||||
SyntaxKind::Rule => self.validate_rule(node),
|
SyntaxKind::Rule => self.validate_rule(node),
|
||||||
SyntaxKind::Set | SyntaxKind::Map => self.validate_set_or_map(node),
|
SyntaxKind::Set | SyntaxKind::Map => self.validate_set_or_map(node),
|
||||||
SyntaxKind::Element => self.validate_element(node),
|
SyntaxKind::Element => self.validate_element(node),
|
||||||
SyntaxKind::Expression | SyntaxKind::BinaryExpr | SyntaxKind::UnaryExpr | SyntaxKind::CallExpr => {
|
SyntaxKind::Expression
|
||||||
self.validate_expression(node)
|
| SyntaxKind::BinaryExpr
|
||||||
},
|
| SyntaxKind::UnaryExpr
|
||||||
|
| SyntaxKind::CallExpr => self.validate_expression(node),
|
||||||
SyntaxKind::IncludeStmt => self.validate_include(node),
|
SyntaxKind::IncludeStmt => self.validate_include(node),
|
||||||
SyntaxKind::DefineStmt => self.validate_define(node),
|
SyntaxKind::DefineStmt => self.validate_define(node),
|
||||||
SyntaxKind::FlushStmt | SyntaxKind::AddStmt | SyntaxKind::DeleteStmt => self.validate_statement(node),
|
SyntaxKind::FlushStmt | SyntaxKind::AddStmt | SyntaxKind::DeleteStmt => {
|
||||||
|
self.validate_statement(node)
|
||||||
|
}
|
||||||
SyntaxKind::Whitespace => Ok(()), // Whitespace is always valid
|
SyntaxKind::Whitespace => Ok(()), // Whitespace is always valid
|
||||||
_ => Ok(()), // Other nodes are generally valid
|
_ => Ok(()), // Other nodes are generally valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1003,15 +1020,24 @@ impl CstValidator {
|
||||||
if let Some(child_node) = child.as_node() {
|
if let Some(child_node) = child.as_node() {
|
||||||
let child_kind = SyntaxKind::from_raw(child_node.kind());
|
let child_kind = SyntaxKind::from_raw(child_node.kind());
|
||||||
match child_kind {
|
match child_kind {
|
||||||
SyntaxKind::Table | SyntaxKind::IncludeStmt | SyntaxKind::DefineStmt |
|
SyntaxKind::Table
|
||||||
SyntaxKind::FlushStmt | SyntaxKind::AddStmt | SyntaxKind::DeleteStmt |
|
| SyntaxKind::IncludeStmt
|
||||||
SyntaxKind::Element | SyntaxKind::Comment | SyntaxKind::Newline |
|
| SyntaxKind::DefineStmt
|
||||||
SyntaxKind::Shebang | SyntaxKind::Whitespace => {
|
| SyntaxKind::FlushStmt
|
||||||
|
| SyntaxKind::AddStmt
|
||||||
|
| SyntaxKind::DeleteStmt
|
||||||
|
| SyntaxKind::Element
|
||||||
|
| SyntaxKind::Comment
|
||||||
|
| SyntaxKind::Newline
|
||||||
|
| SyntaxKind::Shebang
|
||||||
|
| SyntaxKind::Whitespace => {
|
||||||
self.validate(child_node)?;
|
self.validate(child_node)?;
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CstError::UnexpectedToken {
|
return Err(CstError::UnexpectedToken {
|
||||||
expected: "table, include, define, flush, add, delete, element, or comment".to_string(),
|
expected:
|
||||||
|
"table, include, define, flush, add, delete, element, or comment"
|
||||||
|
.to_string(),
|
||||||
found: format!("{:?}", child_kind),
|
found: format!("{:?}", child_kind),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1052,11 +1078,16 @@ impl CstValidator {
|
||||||
if let Some(second_node) = second.as_node() {
|
if let Some(second_node) = second.as_node() {
|
||||||
let second_kind = SyntaxKind::from_raw(second_node.kind());
|
let second_kind = SyntaxKind::from_raw(second_node.kind());
|
||||||
match second_kind {
|
match second_kind {
|
||||||
SyntaxKind::InetKw | SyntaxKind::IpKw | SyntaxKind::Ip6Kw |
|
SyntaxKind::InetKw
|
||||||
SyntaxKind::ArpKw | SyntaxKind::BridgeKw | SyntaxKind::NetdevKw => {},
|
| SyntaxKind::IpKw
|
||||||
|
| SyntaxKind::Ip6Kw
|
||||||
|
| SyntaxKind::ArpKw
|
||||||
|
| SyntaxKind::BridgeKw
|
||||||
|
| SyntaxKind::NetdevKw => {}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CstError::UnexpectedToken {
|
return Err(CstError::UnexpectedToken {
|
||||||
expected: "table family (inet, ip, ip6, arp, bridge, netdev)".to_string(),
|
expected: "table family (inet, ip, ip6, arp, bridge, netdev)"
|
||||||
|
.to_string(),
|
||||||
found: format!("{:?}", second_kind),
|
found: format!("{:?}", second_kind),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
29
src/lexer.rs
29
src/lexer.rs
|
@ -19,7 +19,7 @@ pub type LexResult<T> = Result<T, LexError>;
|
||||||
|
|
||||||
/// Token kinds for nftables configuration files
|
/// Token kinds for nftables configuration files
|
||||||
#[derive(Logos, Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Logos, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
#[logos(skip r"[ \t\f]+")] // Skip whitespace but not newlines
|
#[logos(skip r"[ \t\f]+")] // Skip whitespace but not newlines
|
||||||
pub enum TokenKind {
|
pub enum TokenKind {
|
||||||
// Keywords
|
// Keywords
|
||||||
#[token("table")]
|
#[token("table")]
|
||||||
|
@ -198,7 +198,11 @@ pub enum TokenKind {
|
||||||
NumberLiteral(u64),
|
NumberLiteral(u64),
|
||||||
#[regex(r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+", |lex| lex.slice().to_owned())]
|
#[regex(r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+", |lex| lex.slice().to_owned())]
|
||||||
IpAddress(String),
|
IpAddress(String),
|
||||||
#[regex(r"(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F:]*", ipv6_address, priority = 5)]
|
#[regex(
|
||||||
|
r"(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F:]*",
|
||||||
|
ipv6_address,
|
||||||
|
priority = 5
|
||||||
|
)]
|
||||||
Ipv6Address(String),
|
Ipv6Address(String),
|
||||||
#[regex(r"[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}", |lex| lex.slice().to_owned())]
|
#[regex(r"[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}", |lex| lex.slice().to_owned())]
|
||||||
MacAddress(String),
|
MacAddress(String),
|
||||||
|
@ -245,7 +249,9 @@ impl fmt::Display for TokenKind {
|
||||||
fn string_literal(lex: &mut Lexer<TokenKind>) -> String {
|
fn string_literal(lex: &mut Lexer<TokenKind>) -> String {
|
||||||
let slice = lex.slice();
|
let slice = lex.slice();
|
||||||
// Remove surrounding quotes and process escape sequences
|
// Remove surrounding quotes and process escape sequences
|
||||||
slice[1..slice.len()-1].replace("\\\"", "\"").replace("\\\\", "\\")
|
slice[1..slice.len() - 1]
|
||||||
|
.replace("\\\"", "\"")
|
||||||
|
.replace("\\\\", "\\")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn number_literal(lex: &mut Lexer<TokenKind>) -> Option<u64> {
|
fn number_literal(lex: &mut Lexer<TokenKind>) -> Option<u64> {
|
||||||
|
@ -309,7 +315,7 @@ impl<'a> NftablesLexer<'a> {
|
||||||
let text = &self.source[span.clone()];
|
let text = &self.source[span.clone()];
|
||||||
let range = TextRange::new(
|
let range = TextRange::new(
|
||||||
TextSize::from(span.start as u32),
|
TextSize::from(span.start as u32),
|
||||||
TextSize::from(span.end as u32)
|
TextSize::from(span.end as u32),
|
||||||
);
|
);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
@ -322,8 +328,11 @@ impl<'a> NftablesLexer<'a> {
|
||||||
return Err(LexError::UnterminatedString {
|
return Err(LexError::UnterminatedString {
|
||||||
position: span.start,
|
position: span.start,
|
||||||
});
|
});
|
||||||
} else if text.chars().any(|c| c.is_ascii_digit()) &&
|
} else if text.chars().any(|c| c.is_ascii_digit())
|
||||||
text.chars().any(|c| !c.is_ascii_digit() && c != '.' && c != 'x' && c != 'X') {
|
&& text
|
||||||
|
.chars()
|
||||||
|
.any(|c| !c.is_ascii_digit() && c != '.' && c != 'x' && c != 'X')
|
||||||
|
{
|
||||||
return Err(LexError::InvalidNumber {
|
return Err(LexError::InvalidNumber {
|
||||||
text: text.to_owned(),
|
text: text.to_owned(),
|
||||||
});
|
});
|
||||||
|
@ -349,7 +358,7 @@ impl<'a> NftablesLexer<'a> {
|
||||||
let text = &self.source[span.clone()];
|
let text = &self.source[span.clone()];
|
||||||
let range = TextRange::new(
|
let range = TextRange::new(
|
||||||
TextSize::from(span.start as u32),
|
TextSize::from(span.start as u32),
|
||||||
TextSize::from(span.end as u32)
|
TextSize::from(span.end as u32),
|
||||||
);
|
);
|
||||||
|
|
||||||
let kind = result.unwrap_or(TokenKind::Error);
|
let kind = result.unwrap_or(TokenKind::Error);
|
||||||
|
@ -395,7 +404,11 @@ mod tests {
|
||||||
let mut lexer = NftablesLexer::new(source);
|
let mut lexer = NftablesLexer::new(source);
|
||||||
let tokens = lexer.tokenize().expect("Tokenization should succeed");
|
let tokens = lexer.tokenize().expect("Tokenization should succeed");
|
||||||
|
|
||||||
assert!(tokens.iter().any(|t| matches!(t.kind, TokenKind::CommentLine(_))));
|
assert!(
|
||||||
|
tokens
|
||||||
|
.iter()
|
||||||
|
.any(|t| matches!(t.kind, TokenKind::CommentLine(_)))
|
||||||
|
);
|
||||||
assert!(tokens.iter().any(|t| t.kind == TokenKind::Table));
|
assert!(tokens.iter().any(|t| t.kind == TokenKind::Table));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -4,17 +4,17 @@ mod lexer;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod syntax;
|
mod syntax;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use clap::Parser;
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::cst::CstBuilder;
|
||||||
use crate::lexer::NftablesLexer;
|
use crate::lexer::NftablesLexer;
|
||||||
use crate::parser::Parser as NftablesParser;
|
use crate::parser::Parser as NftablesParser;
|
||||||
use crate::syntax::{FormatConfig, IndentStyle, NftablesFormatter};
|
use crate::syntax::{FormatConfig, IndentStyle, NftablesFormatter};
|
||||||
use crate::cst::CstBuilder;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
enum FormatterError {
|
enum FormatterError {
|
||||||
|
@ -85,14 +85,18 @@ fn process_nftables_config(args: Args) -> Result<()> {
|
||||||
// Use error-recovery tokenization for debug mode
|
// Use error-recovery tokenization for debug mode
|
||||||
lexer.tokenize_with_errors()
|
lexer.tokenize_with_errors()
|
||||||
} else {
|
} else {
|
||||||
lexer.tokenize()
|
lexer
|
||||||
|
.tokenize()
|
||||||
.map_err(|e| FormatterError::ParseError(e.to_string()))?
|
.map_err(|e| FormatterError::ParseError(e.to_string()))?
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.debug {
|
if args.debug {
|
||||||
eprintln!("=== TOKENS ===");
|
eprintln!("=== TOKENS ===");
|
||||||
for (i, token) in tokens.iter().enumerate() {
|
for (i, token) in tokens.iter().enumerate() {
|
||||||
eprintln!("{:3}: {:?} @ {:?} = '{}'", i, token.kind, token.range, token.text);
|
eprintln!(
|
||||||
|
"{:3}: {:?} @ {:?} = '{}'",
|
||||||
|
i, token.kind, token.range, token.text
|
||||||
|
);
|
||||||
}
|
}
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
|
@ -126,7 +130,8 @@ fn process_nftables_config(args: Args) -> Result<()> {
|
||||||
parsed_ruleset.unwrap_or_else(|| crate::ast::Ruleset::new())
|
parsed_ruleset.unwrap_or_else(|| crate::ast::Ruleset::new())
|
||||||
} else {
|
} else {
|
||||||
let mut parser = NftablesParser::new(tokens.clone());
|
let mut parser = NftablesParser::new(tokens.clone());
|
||||||
parser.parse()
|
parser
|
||||||
|
.parse()
|
||||||
.map_err(|e| FormatterError::ParseError(e.to_string()))?
|
.map_err(|e| FormatterError::ParseError(e.to_string()))?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -160,7 +165,8 @@ fn process_nftables_config(args: Args) -> Result<()> {
|
||||||
println!("Formatted output written to: {}", output_file);
|
println!("Formatted output written to: {}", output_file);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
io::stdout().write_all(formatted_output.as_bytes())
|
io::stdout()
|
||||||
|
.write_all(formatted_output.as_bytes())
|
||||||
.with_context(|| "Failed to write to stdout")?;
|
.with_context(|| "Failed to write to stdout")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
255
src/parser.rs
255
src/parser.rs
|
@ -1,12 +1,14 @@
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use crate::lexer::{LexError, Token, TokenKind};
|
use crate::lexer::{LexError, Token, TokenKind};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Result, anyhow};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Parse errors for nftables configuration
|
/// Parse errors for nftables configuration
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
#[error("Unexpected token at line {line}, column {column}: expected {expected}, found '{found}'")]
|
#[error(
|
||||||
|
"Unexpected token at line {line}, column {column}: expected {expected}, found '{found}'"
|
||||||
|
)]
|
||||||
UnexpectedToken {
|
UnexpectedToken {
|
||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
|
@ -123,7 +125,10 @@ impl Parser {
|
||||||
line: 1,
|
line: 1,
|
||||||
column: 1,
|
column: 1,
|
||||||
expected: "include, define, table, or comment".to_string(),
|
expected: "include, define, table, or comment".to_string(),
|
||||||
found: self.peek().map(|t| t.text.clone()).unwrap_or("EOF".to_string()),
|
found: self
|
||||||
|
.peek()
|
||||||
|
.map(|t| t.text.clone())
|
||||||
|
.unwrap_or("EOF".to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,21 +159,23 @@ impl Parser {
|
||||||
self.consume(TokenKind::Include, "Expected 'include'")?;
|
self.consume(TokenKind::Include, "Expected 'include'")?;
|
||||||
|
|
||||||
let path = match self.advance() {
|
let path = match self.advance() {
|
||||||
Some(token) if matches!(token.kind, TokenKind::StringLiteral(_)) => {
|
Some(token) if matches!(token.kind, TokenKind::StringLiteral(_)) => match &token.kind {
|
||||||
match &token.kind {
|
TokenKind::StringLiteral(s) => s.clone(),
|
||||||
TokenKind::StringLiteral(s) => s.clone(),
|
_ => unreachable!(),
|
||||||
_ => unreachable!(),
|
},
|
||||||
}
|
None => {
|
||||||
|
return Err(ParseError::MissingToken {
|
||||||
|
expected: "string literal after 'include'".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(token) => {
|
||||||
|
return Err(ParseError::UnexpectedToken {
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
expected: "string literal".to_string(),
|
||||||
|
found: token.text.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
None => return Err(ParseError::MissingToken {
|
|
||||||
expected: "string literal after 'include'".to_string()
|
|
||||||
}),
|
|
||||||
Some(token) => return Err(ParseError::UnexpectedToken {
|
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
expected: "string literal".to_string(),
|
|
||||||
found: token.text.clone(),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Include { path })
|
Ok(Include { path })
|
||||||
|
@ -179,21 +186,23 @@ impl Parser {
|
||||||
self.consume(TokenKind::Define, "Expected 'define'")?;
|
self.consume(TokenKind::Define, "Expected 'define'")?;
|
||||||
|
|
||||||
let name = match self.advance() {
|
let name = match self.advance() {
|
||||||
Some(token) if matches!(token.kind, TokenKind::Identifier(_)) => {
|
Some(token) if matches!(token.kind, TokenKind::Identifier(_)) => match &token.kind {
|
||||||
match &token.kind {
|
TokenKind::Identifier(s) => s.clone(),
|
||||||
TokenKind::Identifier(s) => s.clone(),
|
_ => unreachable!(),
|
||||||
_ => unreachable!(),
|
},
|
||||||
}
|
None => {
|
||||||
|
return Err(ParseError::MissingToken {
|
||||||
|
expected: "identifier after 'define'".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(token) => {
|
||||||
|
return Err(ParseError::UnexpectedToken {
|
||||||
|
line: 1,
|
||||||
|
column: 1,
|
||||||
|
expected: "identifier".to_string(),
|
||||||
|
found: token.text.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
None => return Err(ParseError::MissingToken {
|
|
||||||
expected: "identifier after 'define'".to_string()
|
|
||||||
}),
|
|
||||||
Some(token) => return Err(ParseError::UnexpectedToken {
|
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
expected: "identifier".to_string(),
|
|
||||||
found: token.text.clone(),
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.consume(TokenKind::Assign, "Expected '=' after define name")?;
|
self.consume(TokenKind::Assign, "Expected '=' after define name")?;
|
||||||
|
@ -232,10 +241,14 @@ impl Parser {
|
||||||
Some(TokenKind::Newline) => {
|
Some(TokenKind::Newline) => {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
_ => { return Err(ParseError::SemanticError {
|
_ => {
|
||||||
message: format!("Unexpected token in table: {}",
|
return Err(ParseError::SemanticError {
|
||||||
self.peek().map(|t| t.text.as_str()).unwrap_or("EOF")),
|
message: format!(
|
||||||
}.into());
|
"Unexpected token in table: {}",
|
||||||
|
self.peek().map(|t| t.text.as_str()).unwrap_or("EOF")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
|
@ -337,8 +350,8 @@ impl Parser {
|
||||||
// Parse expressions and action
|
// Parse expressions and action
|
||||||
while !self.current_token_is(&TokenKind::Newline)
|
while !self.current_token_is(&TokenKind::Newline)
|
||||||
&& !self.current_token_is(&TokenKind::RightBrace)
|
&& !self.current_token_is(&TokenKind::RightBrace)
|
||||||
&& !self.is_at_end() {
|
&& !self.is_at_end()
|
||||||
|
{
|
||||||
// Check for actions first
|
// Check for actions first
|
||||||
match self.peek().map(|t| &t.kind) {
|
match self.peek().map(|t| &t.kind) {
|
||||||
Some(TokenKind::Accept) => {
|
Some(TokenKind::Accept) => {
|
||||||
|
@ -415,7 +428,8 @@ impl Parser {
|
||||||
} else {
|
} else {
|
||||||
return Err(ParseError::InvalidStatement {
|
return Err(ParseError::InvalidStatement {
|
||||||
message: "Expected string literal after 'comment'".to_string(),
|
message: "Expected string literal after 'comment'".to_string(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -451,16 +465,16 @@ impl Parser {
|
||||||
fn parse_comparison_expression(&mut self) -> Result<Expression> {
|
fn parse_comparison_expression(&mut self) -> Result<Expression> {
|
||||||
let mut expr = self.parse_range_expression()?;
|
let mut expr = self.parse_range_expression()?;
|
||||||
|
|
||||||
while let Some(token) = self.peek() {
|
while let Some(token) = self.peek() {
|
||||||
let operator = match &token.kind {
|
let operator = match &token.kind {
|
||||||
TokenKind::Eq => BinaryOperator::Eq,
|
TokenKind::Eq => BinaryOperator::Eq,
|
||||||
TokenKind::Ne => BinaryOperator::Ne,
|
TokenKind::Ne => BinaryOperator::Ne,
|
||||||
TokenKind::Lt => BinaryOperator::Lt,
|
TokenKind::Lt => BinaryOperator::Lt,
|
||||||
TokenKind::Le => BinaryOperator::Le,
|
TokenKind::Le => BinaryOperator::Le,
|
||||||
TokenKind::Gt => BinaryOperator::Gt,
|
TokenKind::Gt => BinaryOperator::Gt,
|
||||||
TokenKind::Ge => BinaryOperator::Ge,
|
TokenKind::Ge => BinaryOperator::Ge,
|
||||||
_ => break,
|
_ => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.advance(); // consume operator
|
self.advance(); // consume operator
|
||||||
let right = self.parse_range_expression()?;
|
let right = self.parse_range_expression()?;
|
||||||
|
@ -664,19 +678,22 @@ impl Parser {
|
||||||
// Check for rate expression (e.g., 10/minute)
|
// Check for rate expression (e.g., 10/minute)
|
||||||
if self.current_token_is(&TokenKind::Slash) {
|
if self.current_token_is(&TokenKind::Slash) {
|
||||||
self.advance(); // consume '/'
|
self.advance(); // consume '/'
|
||||||
if let Some(token) = self.peek() {
|
if let Some(token) = self.peek() {
|
||||||
if matches!(token.kind, TokenKind::Identifier(_)) {
|
if matches!(token.kind, TokenKind::Identifier(_)) {
|
||||||
let unit = self.advance().unwrap().text.clone();
|
let unit = self.advance().unwrap().text.clone();
|
||||||
Ok(Expression::String(format!("{}/{}", num, unit)))
|
Ok(Expression::String(format!("{}/{}", num, unit)))
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::InvalidExpression {
|
Err(ParseError::InvalidExpression {
|
||||||
message: "Expected identifier after '/' in rate expression".to_string(),
|
message: "Expected identifier after '/' in rate expression"
|
||||||
}.into())
|
.to_string(),
|
||||||
}
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::InvalidExpression {
|
Err(ParseError::InvalidExpression {
|
||||||
message: "Expected identifier after '/' in rate expression".to_string(),
|
message: "Expected identifier after '/' in rate expression".to_string(),
|
||||||
}.into())
|
}
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(Expression::Number(num))
|
Ok(Expression::Number(num))
|
||||||
|
@ -694,12 +711,14 @@ impl Parser {
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::InvalidExpression {
|
Err(ParseError::InvalidExpression {
|
||||||
message: "Expected number after '/' in CIDR notation".to_string(),
|
message: "Expected number after '/' in CIDR notation".to_string(),
|
||||||
}.into())
|
}
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::InvalidExpression {
|
Err(ParseError::InvalidExpression {
|
||||||
message: "Expected number after '/' in CIDR notation".to_string(),
|
message: "Expected number after '/' in CIDR notation".to_string(),
|
||||||
}.into())
|
}
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
// Check for port specification (e.g., 192.168.1.100:80)
|
// Check for port specification (e.g., 192.168.1.100:80)
|
||||||
} else if self.current_token_is(&TokenKind::Colon) {
|
} else if self.current_token_is(&TokenKind::Colon) {
|
||||||
|
@ -710,13 +729,17 @@ impl Parser {
|
||||||
Ok(Expression::String(format!("{}:{}", addr, port)))
|
Ok(Expression::String(format!("{}:{}", addr, port)))
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::InvalidExpression {
|
Err(ParseError::InvalidExpression {
|
||||||
message: "Expected number after ':' in address:port specification".to_string(),
|
message: "Expected number after ':' in address:port specification"
|
||||||
}.into())
|
.to_string(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::InvalidExpression {
|
Err(ParseError::InvalidExpression {
|
||||||
message: "Expected number after ':' in address:port specification".to_string(),
|
message: "Expected number after ':' in address:port specification"
|
||||||
}.into())
|
.to_string(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(Expression::IpAddress(addr))
|
Ok(Expression::IpAddress(addr))
|
||||||
|
@ -751,12 +774,13 @@ impl Parser {
|
||||||
self.consume(TokenKind::RightBrace, "Expected '}' to close set")?;
|
self.consume(TokenKind::RightBrace, "Expected '}' to close set")?;
|
||||||
Ok(Expression::Set(elements))
|
Ok(Expression::Set(elements))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => Err(ParseError::InvalidExpression {
|
||||||
Err(ParseError::InvalidExpression {
|
message: format!(
|
||||||
message: format!("Unexpected token in expression: {}",
|
"Unexpected token in expression: {}",
|
||||||
self.peek().map(|t| t.text.as_str()).unwrap_or("EOF")),
|
self.peek().map(|t| t.text.as_str()).unwrap_or("EOF")
|
||||||
}.into())
|
),
|
||||||
}
|
}
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -822,12 +846,12 @@ impl Parser {
|
||||||
let token_clone = token.clone();
|
let token_clone = token.clone();
|
||||||
Err(self.unexpected_token_error_with_token(
|
Err(self.unexpected_token_error_with_token(
|
||||||
"family (ip, ip6, inet, arp, bridge, netdev)".to_string(),
|
"family (ip, ip6, inet, arp, bridge, netdev)".to_string(),
|
||||||
&token_clone
|
&token_clone,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => Err(ParseError::MissingToken {
|
None => Err(ParseError::MissingToken {
|
||||||
expected: "family".to_string()
|
expected: "family".to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -842,12 +866,12 @@ impl Parser {
|
||||||
let token_clone = token.clone();
|
let token_clone = token.clone();
|
||||||
Err(self.unexpected_token_error_with_token(
|
Err(self.unexpected_token_error_with_token(
|
||||||
"chain type (filter, nat, route)".to_string(),
|
"chain type (filter, nat, route)".to_string(),
|
||||||
&token_clone
|
&token_clone,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => Err(ParseError::MissingToken {
|
None => Err(ParseError::MissingToken {
|
||||||
expected: "chain type".to_string()
|
expected: "chain type".to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -864,12 +888,12 @@ impl Parser {
|
||||||
let token_clone = token.clone();
|
let token_clone = token.clone();
|
||||||
Err(self.unexpected_token_error_with_token(
|
Err(self.unexpected_token_error_with_token(
|
||||||
"hook (input, output, forward, prerouting, postrouting)".to_string(),
|
"hook (input, output, forward, prerouting, postrouting)".to_string(),
|
||||||
&token_clone
|
&token_clone,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => Err(ParseError::MissingToken {
|
None => Err(ParseError::MissingToken {
|
||||||
expected: "hook".to_string()
|
expected: "hook".to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -879,7 +903,10 @@ impl Parser {
|
||||||
Some(token) => match token.kind {
|
Some(token) => match token.kind {
|
||||||
TokenKind::Accept => Ok(Policy::Accept),
|
TokenKind::Accept => Ok(Policy::Accept),
|
||||||
TokenKind::Drop => Ok(Policy::Drop),
|
TokenKind::Drop => Ok(Policy::Drop),
|
||||||
_ => Err(anyhow!("Expected policy (accept, drop), got: {}", token.text)),
|
_ => Err(anyhow!(
|
||||||
|
"Expected policy (accept, drop), got: {}",
|
||||||
|
token.text
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
None => Err(anyhow!("Expected policy")),
|
None => Err(anyhow!("Expected policy")),
|
||||||
}
|
}
|
||||||
|
@ -887,12 +914,10 @@ impl Parser {
|
||||||
|
|
||||||
fn parse_identifier(&mut self) -> Result<String> {
|
fn parse_identifier(&mut self) -> Result<String> {
|
||||||
match self.advance() {
|
match self.advance() {
|
||||||
Some(token) if matches!(token.kind, TokenKind::Identifier(_)) => {
|
Some(token) if matches!(token.kind, TokenKind::Identifier(_)) => match &token.kind {
|
||||||
match &token.kind {
|
TokenKind::Identifier(s) => Ok(s.clone()),
|
||||||
TokenKind::Identifier(s) => Ok(s.clone()),
|
_ => unreachable!(),
|
||||||
_ => unreachable!(),
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(token) => Err(anyhow!("Expected identifier, got: {}", token.text)),
|
Some(token) => Err(anyhow!("Expected identifier, got: {}", token.text)),
|
||||||
None => Err(anyhow!("Expected identifier")),
|
None => Err(anyhow!("Expected identifier")),
|
||||||
}
|
}
|
||||||
|
@ -905,17 +930,33 @@ impl Parser {
|
||||||
match &token.kind {
|
match &token.kind {
|
||||||
TokenKind::Identifier(s) => Ok(s.clone()),
|
TokenKind::Identifier(s) => Ok(s.clone()),
|
||||||
// Allow keywords to be used as identifiers in certain contexts
|
// Allow keywords to be used as identifiers in certain contexts
|
||||||
TokenKind::Filter | TokenKind::Nat | TokenKind::Route |
|
TokenKind::Filter
|
||||||
TokenKind::Input | TokenKind::Output | TokenKind::Forward |
|
| TokenKind::Nat
|
||||||
TokenKind::Prerouting | TokenKind::Postrouting |
|
| TokenKind::Route
|
||||||
TokenKind::Accept | TokenKind::Drop | TokenKind::Reject |
|
| TokenKind::Input
|
||||||
TokenKind::State | TokenKind::Ct | TokenKind::Type | TokenKind::Hook |
|
| TokenKind::Output
|
||||||
TokenKind::Priority | TokenKind::Policy |
|
| TokenKind::Forward
|
||||||
TokenKind::Tcp | TokenKind::Udp | TokenKind::Icmp | TokenKind::Icmpv6 |
|
| TokenKind::Prerouting
|
||||||
TokenKind::Ip | TokenKind::Ip6 => {
|
| TokenKind::Postrouting
|
||||||
Ok(token.text.clone())
|
| TokenKind::Accept
|
||||||
}
|
| TokenKind::Drop
|
||||||
_ => Err(anyhow!("Expected identifier or keyword, got: {}", token.text)),
|
| TokenKind::Reject
|
||||||
|
| TokenKind::State
|
||||||
|
| TokenKind::Ct
|
||||||
|
| TokenKind::Type
|
||||||
|
| TokenKind::Hook
|
||||||
|
| TokenKind::Priority
|
||||||
|
| TokenKind::Policy
|
||||||
|
| TokenKind::Tcp
|
||||||
|
| TokenKind::Udp
|
||||||
|
| TokenKind::Icmp
|
||||||
|
| TokenKind::Icmpv6
|
||||||
|
| TokenKind::Ip
|
||||||
|
| TokenKind::Ip6 => Ok(token.text.clone()),
|
||||||
|
_ => Err(anyhow!(
|
||||||
|
"Expected identifier or keyword, got: {}",
|
||||||
|
token.text
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Err(anyhow!("Expected identifier or keyword")),
|
None => Err(anyhow!("Expected identifier or keyword")),
|
||||||
|
@ -924,26 +965,32 @@ impl Parser {
|
||||||
|
|
||||||
fn parse_string_or_identifier(&mut self) -> Result<String> {
|
fn parse_string_or_identifier(&mut self) -> Result<String> {
|
||||||
match self.advance() {
|
match self.advance() {
|
||||||
Some(token) if matches!(token.kind, TokenKind::Identifier(_) | TokenKind::StringLiteral(_)) => {
|
Some(token)
|
||||||
|
if matches!(
|
||||||
|
token.kind,
|
||||||
|
TokenKind::Identifier(_) | TokenKind::StringLiteral(_)
|
||||||
|
) =>
|
||||||
|
{
|
||||||
match &token.kind {
|
match &token.kind {
|
||||||
TokenKind::Identifier(s) => Ok(s.clone()),
|
TokenKind::Identifier(s) => Ok(s.clone()),
|
||||||
TokenKind::StringLiteral(s) => Ok(s.clone()),
|
TokenKind::StringLiteral(s) => Ok(s.clone()),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(token) => Err(anyhow!("Expected string or identifier, got: {}", token.text)),
|
Some(token) => Err(anyhow!(
|
||||||
|
"Expected string or identifier, got: {}",
|
||||||
|
token.text
|
||||||
|
)),
|
||||||
None => Err(anyhow!("Expected string or identifier")),
|
None => Err(anyhow!("Expected string or identifier")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_number(&mut self) -> Result<u64> {
|
fn parse_number(&mut self) -> Result<u64> {
|
||||||
match self.advance() {
|
match self.advance() {
|
||||||
Some(token) if matches!(token.kind, TokenKind::NumberLiteral(_)) => {
|
Some(token) if matches!(token.kind, TokenKind::NumberLiteral(_)) => match &token.kind {
|
||||||
match &token.kind {
|
TokenKind::NumberLiteral(n) => Ok(*n),
|
||||||
TokenKind::NumberLiteral(n) => Ok(*n),
|
_ => unreachable!(),
|
||||||
_ => unreachable!(),
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(token) => Err(anyhow!("Expected number, got: {}", token.text)),
|
Some(token) => Err(anyhow!("Expected number, got: {}", token.text)),
|
||||||
None => Err(anyhow!("Expected number")),
|
None => Err(anyhow!("Expected number")),
|
||||||
}
|
}
|
||||||
|
@ -1029,11 +1076,7 @@ impl Parser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unexpected_token_error_with_token(
|
fn unexpected_token_error_with_token(&self, expected: String, token: &Token) -> ParseError {
|
||||||
&self,
|
|
||||||
expected: String,
|
|
||||||
token: &Token,
|
|
||||||
) -> ParseError {
|
|
||||||
ParseError::UnexpectedToken {
|
ParseError::UnexpectedToken {
|
||||||
line: 1, // TODO: Calculate line from range
|
line: 1, // TODO: Calculate line from range
|
||||||
column: token.range.start().into(),
|
column: token.range.start().into(),
|
||||||
|
|
109
src/syntax.rs
109
src/syntax.rs
|
@ -71,28 +71,28 @@ impl NftablesFormatter {
|
||||||
self.format_include(&mut output, include, 0);
|
self.format_include(&mut output, include, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add separator if we have includes
|
// Add separator if we have includes
|
||||||
if !ruleset.includes.is_empty() {
|
if !ruleset.includes.is_empty() {
|
||||||
self.add_separator(&mut output);
|
self.add_separator(&mut output);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format defines
|
// Format defines
|
||||||
for define in &ruleset.defines {
|
for define in &ruleset.defines {
|
||||||
self.format_define(&mut output, define, 0);
|
self.format_define(&mut output, define, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add separator if we have defines
|
// Add separator if we have defines
|
||||||
if !ruleset.defines.is_empty() {
|
if !ruleset.defines.is_empty() {
|
||||||
self.add_separator(&mut output);
|
self.add_separator(&mut output);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format tables
|
// Format tables
|
||||||
let mut table_iter = ruleset.tables.values().peekable();
|
let mut table_iter = ruleset.tables.values().peekable();
|
||||||
while let Some(table) = table_iter.next() {
|
while let Some(table) = table_iter.next() {
|
||||||
self.format_table(&mut output, table, 0); // Add separator between tables
|
self.format_table(&mut output, table, 0); // Add separator between tables
|
||||||
if table_iter.peek().is_some() {
|
if table_iter.peek().is_some() {
|
||||||
self.add_separator(&mut output);
|
self.add_separator(&mut output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure file ends with newline
|
// Ensure file ends with newline
|
||||||
|
@ -104,44 +104,62 @@ impl NftablesFormatter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_include(&self, output: &mut String, include: &Include, level: usize) {
|
fn format_include(&self, output: &mut String, include: &Include, level: usize) {
|
||||||
let indent = self.config.indent_style.format(level, self.config.spaces_per_level);
|
let indent = self
|
||||||
|
.config
|
||||||
|
.indent_style
|
||||||
|
.format(level, self.config.spaces_per_level);
|
||||||
writeln!(output, "{}include \"{}\"", indent, include.path).unwrap();
|
writeln!(output, "{}include \"{}\"", indent, include.path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_define(&self, output: &mut String, define: &Define, level: usize) {
|
fn format_define(&self, output: &mut String, define: &Define, level: usize) {
|
||||||
let indent = self.config.indent_style.format(level, self.config.spaces_per_level);
|
let indent = self
|
||||||
|
.config
|
||||||
|
.indent_style
|
||||||
|
.format(level, self.config.spaces_per_level);
|
||||||
write!(output, "{}define {} = ", indent, define.name).unwrap();
|
write!(output, "{}define {} = ", indent, define.name).unwrap();
|
||||||
self.format_expression(output, &define.value);
|
self.format_expression(output, &define.value);
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_table(&self, output: &mut String, table: &Table, level: usize) {
|
fn format_table(&self, output: &mut String, table: &Table, level: usize) {
|
||||||
let indent = self.config.indent_style.format(level, self.config.spaces_per_level);
|
let indent = self
|
||||||
|
.config
|
||||||
|
.indent_style
|
||||||
|
.format(level, self.config.spaces_per_level);
|
||||||
|
|
||||||
writeln!(output, "{}table {} {} {{", indent, table.family, table.name).unwrap();
|
writeln!(output, "{}table {} {} {{", indent, table.family, table.name).unwrap();
|
||||||
|
|
||||||
// Format chains
|
// Format chains
|
||||||
let mut chain_iter = table.chains.values().peekable();
|
let mut chain_iter = table.chains.values().peekable();
|
||||||
while let Some(chain) = chain_iter.next() {
|
while let Some(chain) = chain_iter.next() {
|
||||||
self.format_chain(output, chain, level + 1); // Add separator between chains
|
self.format_chain(output, chain, level + 1); // Add separator between chains
|
||||||
if chain_iter.peek().is_some() {
|
if chain_iter.peek().is_some() {
|
||||||
self.add_separator(output);
|
self.add_separator(output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeln!(output, "{}}}", indent).unwrap();
|
writeln!(output, "{}}}", indent).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_chain(&self, output: &mut String, chain: &Chain, level: usize) {
|
fn format_chain(&self, output: &mut String, chain: &Chain, level: usize) {
|
||||||
let indent = self.config.indent_style.format(level, self.config.spaces_per_level);
|
let indent = self
|
||||||
|
.config
|
||||||
|
.indent_style
|
||||||
|
.format(level, self.config.spaces_per_level);
|
||||||
|
|
||||||
writeln!(output, "{}chain {} {{", indent, chain.name).unwrap();
|
writeln!(output, "{}chain {} {{", indent, chain.name).unwrap();
|
||||||
|
|
||||||
// Format chain properties
|
// Format chain properties
|
||||||
if let Some(chain_type) = &chain.chain_type {
|
if let Some(chain_type) = &chain.chain_type {
|
||||||
write!(output, "{}type {}",
|
write!(
|
||||||
self.config.indent_style.format(level + 1, self.config.spaces_per_level),
|
output,
|
||||||
chain_type).unwrap();
|
"{}type {}",
|
||||||
|
self.config
|
||||||
|
.indent_style
|
||||||
|
.format(level + 1, self.config.spaces_per_level),
|
||||||
|
chain_type
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
if let Some(hook) = &chain.hook {
|
if let Some(hook) = &chain.hook {
|
||||||
write!(output, " hook {}", hook).unwrap();
|
write!(output, " hook {}", hook).unwrap();
|
||||||
|
@ -179,7 +197,10 @@ impl NftablesFormatter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_rule(&self, output: &mut String, rule: &Rule, level: usize) {
|
fn format_rule(&self, output: &mut String, rule: &Rule, level: usize) {
|
||||||
let indent = self.config.indent_style.format(level, self.config.spaces_per_level);
|
let indent = self
|
||||||
|
.config
|
||||||
|
.indent_style
|
||||||
|
.format(level, self.config.spaces_per_level);
|
||||||
|
|
||||||
write!(output, "{}", indent).unwrap();
|
write!(output, "{}", indent).unwrap();
|
||||||
|
|
||||||
|
@ -212,7 +233,11 @@ impl NftablesFormatter {
|
||||||
Expression::Ipv6Address(addr) => write!(output, "{}", addr).unwrap(),
|
Expression::Ipv6Address(addr) => write!(output, "{}", addr).unwrap(),
|
||||||
Expression::MacAddress(addr) => write!(output, "{}", addr).unwrap(),
|
Expression::MacAddress(addr) => write!(output, "{}", addr).unwrap(),
|
||||||
|
|
||||||
Expression::Binary { left, operator, right } => {
|
Expression::Binary {
|
||||||
|
left,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
} => {
|
||||||
self.format_expression(output, left);
|
self.format_expression(output, left);
|
||||||
write!(output, " {} ", operator).unwrap();
|
write!(output, " {} ", operator).unwrap();
|
||||||
self.format_expression(output, right);
|
self.format_expression(output, right);
|
||||||
|
@ -279,7 +304,10 @@ impl std::str::FromStr for IndentStyle {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"tabs" | "tab" => Ok(IndentStyle::Tabs),
|
"tabs" | "tab" => Ok(IndentStyle::Tabs),
|
||||||
"spaces" | "space" => Ok(IndentStyle::Spaces),
|
"spaces" | "space" => Ok(IndentStyle::Spaces),
|
||||||
_ => Err(format!("Invalid indent style: {}. Use 'tabs' or 'spaces'", s)),
|
_ => Err(format!(
|
||||||
|
"Invalid indent style: {}. Use 'tabs' or 'spaces'",
|
||||||
|
s
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -290,21 +318,20 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_simple_table() {
|
fn test_format_simple_table() {
|
||||||
let table = Table::new(Family::Inet, "test".to_string())
|
let table = Table::new(Family::Inet, "test".to_string()).add_chain(
|
||||||
.add_chain(
|
Chain::new("input".to_string())
|
||||||
Chain::new("input".to_string())
|
.with_type(ChainType::Filter)
|
||||||
.with_type(ChainType::Filter)
|
.with_hook(Hook::Input)
|
||||||
.with_hook(Hook::Input)
|
.with_priority(0)
|
||||||
.with_priority(0)
|
.with_policy(Policy::Accept)
|
||||||
.with_policy(Policy::Accept)
|
.add_rule(Rule::new(
|
||||||
.add_rule(Rule::new(
|
vec![Expression::Interface {
|
||||||
vec![Expression::Interface {
|
direction: InterfaceDirection::Input,
|
||||||
direction: InterfaceDirection::Input,
|
name: "lo".to_string(),
|
||||||
name: "lo".to_string(),
|
}],
|
||||||
}],
|
Action::Accept,
|
||||||
Action::Accept,
|
)),
|
||||||
))
|
);
|
||||||
);
|
|
||||||
|
|
||||||
let formatter = NftablesFormatter::new(FormatConfig::default());
|
let formatter = NftablesFormatter::new(FormatConfig::default());
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue