///|
pub struct ZoneToken {
text_value : String
quoted_value : Bool
span_value : SourceSpan
} derive(Eq, Debug)
///|
pub struct ZoneStatement {
token_values : Array[ZoneToken]
} derive(Eq, Debug)
///|
fn lexer_error(
code : ZoneErrorCode,
message : String,
line : Int,
column : Int,
) -> ZoneError {
ZoneError::new(code, message, SourceSpan::point(line, column))
}
///|
fn ascii_character(code : Int) -> String {
match code.to_char() {
Some(value) => value.to_string()
None => ""
}
}
///|
fn is_zone_space(code : Int) -> Bool {
code == 32 || code == 9
}
///|
fn is_decimal_digit(code : Int) -> Bool {
code >= 48 && code <= 57
}
///|
fn finish_zone_token(
tokens : Array[ZoneToken],
text : String,
quoted : Bool,
start_line : Int,
start_column : Int,
end_line : Int,
end_column : Int,
) -> Unit {
tokens.push({
text_value: text,
quoted_value: quoted,
span_value: SourceSpan::range(
start_line, start_column, end_line, end_column,
),
})
}
///|
/// Tokenize BIND-style zone text while preserving logical statements.
pub fn lex_zone(
text : String,
max_tokens? : Int = 100000,
max_token_bytes? : Int = 65535,
) -> Result[Array[ZoneStatement], ZoneError] {
if max_tokens < 0 || max_token_bytes < 0 {
return Err(
lexer_error(TokenLimitExceeded, "lexer limits cannot be negative", 0, 0),
)
}
let statements : Array[ZoneStatement] = []
let mut tokens : Array[ZoneToken] = []
let mut current = ""
let mut token_active = false
let mut token_quoted = false
let mut token_start_line = 0
let mut token_start_column = 0
let mut in_quote = false
let mut parenthesis_depth = 0
let mut line = 0
let mut column = 0
let mut index = 0
let mut token_count = 0
while index < text.length() {
let code = text[index].to_int()
if code > 127 {
return Err(
lexer_error(
InvalidCharacter,
"zone text must use ASCII syntax characters",
line,
column,
),
)
}
if code == 92 {
if !token_active {
token_active = true
token_start_line = line
token_start_column = column
}
if index + 1 >= text.length() {
return Err(
lexer_error(
UnterminatedEscape,
"zone token ends with an incomplete escape",
line,
column,
),
)
}
if index + 3 < text.length() &&
is_decimal_digit(text[index + 1].to_int()) &&
is_decimal_digit(text[index + 2].to_int()) &&
is_decimal_digit(text[index + 3].to_int()) {
let value = (text[index + 1].to_int() - 48) * 100 +
(text[index + 2].to_int() - 48) * 10 +
(text[index + 3].to_int() - 48)
if value > 255 {
return Err(
lexer_error(
InvalidCharacter,
"decimal escape exceeds one octet",
line,
column,
),
)
}
current = current + ascii_character(value)
index = index + 4
column = column + 4
} else {
let escaped = text[index + 1].to_int()
if escaped == 13 || escaped == 10 {
return Err(
lexer_error(
InvalidCharacter,
"escaped line ending is not supported",
line,
column,
),
)
}
current = current + ascii_character(escaped)
index = index + 2
column = column + 2
}
if current.length() > max_token_bytes {
return Err(
lexer_error(
TokenLimitExceeded,
"zone token exceeds the configured byte limit",
line,
column,
),
)
}
continue
}
if in_quote {
if code == 34 {
in_quote = false
index = index + 1
column = column + 1
} else if code == 13 || code == 10 {
return Err(
lexer_error(
UnterminatedQuote,
"quoted zone token crosses a physical line",
line,
column,
),
)
} else {
current = current + ascii_character(code)
index = index + 1
column = column + 1
}
if current.length() > max_token_bytes {
return Err(
lexer_error(
TokenLimitExceeded,
"quoted token exceeds the configured byte limit",
line,
column,
),
)
}
continue
}
if code == 34 {
if !token_active {
token_active = true
token_start_line = line
token_start_column = column
}
token_quoted = true
in_quote = true
index = index + 1
column = column + 1
} else if code == 59 {
if token_active {
finish_zone_token(
tokens, current, token_quoted, token_start_line, token_start_column, line,
column,
)
token_count = token_count + 1
current = ""
token_active = false
token_quoted = false
}
while index < text.length() &&
text[index].to_int() != 13 &&
text[index].to_int() != 10 {
index = index + 1
column = column + 1
}
} else if code == 40 || code == 41 {
if token_active {
finish_zone_token(
tokens, current, token_quoted, token_start_line, token_start_column, line,
column,
)
token_count = token_count + 1
current = ""
token_active = false
token_quoted = false
}
if code == 40 {
parenthesis_depth = parenthesis_depth + 1
} else if parenthesis_depth == 0 {
return Err(
lexer_error(
UnbalancedParenthesis,
"zone text has an unmatched closing parenthesis",
line,
column,
),
)
} else {
parenthesis_depth = parenthesis_depth - 1
}
index = index + 1
column = column + 1
} else if is_zone_space(code) {
if token_active {
finish_zone_token(
tokens, current, token_quoted, token_start_line, token_start_column, line,
column,
)
token_count = token_count + 1
current = ""
token_active = false
token_quoted = false
}
index = index + 1
column = column + 1
} else if code == 13 || code == 10 {
if token_active {
finish_zone_token(
tokens, current, token_quoted, token_start_line, token_start_column, line,
column,
)
token_count = token_count + 1
current = ""
token_active = false
token_quoted = false
}
if parenthesis_depth == 0 && tokens.length() > 0 {
statements.push({ token_values: tokens })
tokens = []
}
if code == 13 &&
index + 1 < text.length() &&
text[index + 1].to_int() == 10 {
index = index + 2
} else {
index = index + 1
}
line = line + 1
column = 0
} else {
if !token_active {
token_active = true
token_start_line = line
token_start_column = column
}
current = current + ascii_character(code)
if current.length() > max_token_bytes {
return Err(
lexer_error(
TokenLimitExceeded,
"zone token exceeds the configured byte limit",
line,
column,
),
)
}
index = index + 1
column = column + 1
}
if token_count > max_tokens {
return Err(
lexer_error(
TokenLimitExceeded,
"zone text exceeds the configured token limit",
line,
column,
),
)
}
}
if in_quote {
return Err(
lexer_error(
UnterminatedQuote,
"zone text ends inside a quoted token",
line,
column,
),
)
}
if parenthesis_depth != 0 {
return Err(
lexer_error(
UnbalancedParenthesis,
"zone text ends inside a parenthesized record",
line,
column,
),
)
}
if token_active {
finish_zone_token(
tokens, current, token_quoted, token_start_line, token_start_column, line,
column,
)
token_count = token_count + 1
}
if token_count > max_tokens {
return Err(
lexer_error(
TokenLimitExceeded,
"zone text exceeds the configured token limit",
line,
column,
),
)
}
if tokens.length() > 0 {
statements.push({ token_values: tokens })
}
Ok(statements)
}
///|
pub fn ZoneToken::text(self : ZoneToken) -> String {
self.text_value
}
///|
pub fn ZoneToken::quoted(self : ZoneToken) -> Bool {
self.quoted_value
}
///|
pub fn ZoneToken::span(self : ZoneToken) -> SourceSpan {
self.span_value
}
///|
pub fn ZoneStatement::tokens(self : ZoneStatement) -> Array[ZoneToken] {
self.token_values.copy()
}
///|
pub fn ZoneStatement::token_texts(self : ZoneStatement) -> Array[String] {
let result : Array[String] = []
for token in self.token_values {
result.push(token.text())
}
result
}