///|
priv enum QuoteMode {
Unquoted
SingleQuoted
DoubleQuoted
} derive(Eq)
///|
priv struct Lexeme {
value : String
span : SourceSpan
}
///|
let max_line_characters : Int = 65_536
///|
let max_source_bytes : Int = 8 * 1024 * 1024
///|
fn lower_ascii(value : String) -> String {
let output = StringBuilder()
for char in value {
output.write_char(char.to_ascii_lowercase())
}
output.to_string()
}
///|
fn location(path : String, line : Int, column : Int) -> SourceLocation {
{ path, line, column }
}
///|
fn span_from(start : SourceLocation, end_ : SourceLocation) -> SourceSpan {
{ start, end_ }
}
///|
fn invalid_control(char : Char) -> Bool {
char.is_control() && char != '\t' && char != '\r'
}
///|
fn validate_character(
char : Char,
path : String,
line : Int,
column : Int,
) -> Unit raise ParseError {
let at = location(path, line, column)
if char == '\u{00}' {
raise UnexpectedNul(location=at)
}
if invalid_control(char) {
raise InvalidDirective(location=at, message="unsupported control character")
}
}
///|
fn finish_lexeme(
tokens : Array[Lexeme],
output : StringBuilder,
start : SourceLocation,
end_ : SourceLocation,
) -> Unit {
tokens.push({ value: output.to_string(), span: span_from(start, end_) })
output.reset()
}
///|
fn write_backslash(
characters : Array[Char],
index : Int,
output : StringBuilder,
) -> Int {
if index + 1 < characters.length() &&
(characters[index + 1] == '\'' || characters[index + 1] == '"') {
output.write_char(characters[index + 1])
2
} else {
output.write_char('\\')
1
}
}
///|
fn utf8_width(char : Char) -> Int {
let codepoint = char.to_int()
if codepoint <= 0x7f {
1
} else if codepoint <= 0x7ff {
2
} else if codepoint <= 0xffff {
3
} else {
4
}
}
///|
fn source_exceeds_byte_limit(source : String) -> Bool {
let mut byte_count = 0
for char in source {
let width = utf8_width(char)
if byte_count > max_source_bytes - width {
return true
}
byte_count += width
}
false
}
///|
/// Decode one physical configuration line. A `#` begins a comment only at a
/// token boundary. OpenSSH preserves ordinary backslashes; only a backslash
/// directly before either quote character quotes that character.
fn lex_line(
raw_line : String,
path : String,
line_number : Int,
) -> Array[Lexeme] raise ParseError {
let characters = raw_line.to_array()
let first = location(path, line_number, 1)
if characters.length() > max_line_characters {
raise LineTooLong(location=first, limit=max_line_characters)
}
let tokens : Array[Lexeme] = []
let output = StringBuilder()
let mut index = 0
let mut column = 1
let mut active = false
let mut start = first
let mut quote_start = first
let mut mode = Unquoted
while index < characters.length() {
let char = characters[index]
validate_character(char, path, line_number, column)
if !active {
if char == '#' {
break
}
if char == ' ' || char == '\t' {
index += 1
column += 1
continue
}
active = true
start = location(path, line_number, column)
if char == '\'' {
mode = SingleQuoted
quote_start = start
index += 1
column += 1
continue
}
if char == '"' {
mode = DoubleQuoted
quote_start = start
index += 1
column += 1
continue
}
if char == '\\' {
let consumed = write_backslash(characters, index, output)
index += consumed
column += consumed
continue
}
output.write_char(char)
index += 1
column += 1
continue
}
match mode {
Unquoted => {
if char == ' ' || char == '\t' {
finish_lexeme(
tokens,
output,
start,
location(path, line_number, column),
)
active = false
index += 1
column += 1
continue
}
if char == '\'' {
mode = SingleQuoted
quote_start = location(path, line_number, column)
index += 1
column += 1
continue
}
if char == '"' {
mode = DoubleQuoted
quote_start = location(path, line_number, column)
index += 1
column += 1
continue
}
if char == '\\' {
let consumed = write_backslash(characters, index, output)
index += consumed
column += consumed
continue
}
output.write_char(char)
index += 1
column += 1
}
SingleQuoted => {
if char == '\\' {
let consumed = write_backslash(characters, index, output)
index += consumed
column += consumed
continue
} else if char == '\'' {
mode = Unquoted
} else {
output.write_char(char)
}
index += 1
column += 1
}
DoubleQuoted => {
if char == '\\' {
let consumed = write_backslash(characters, index, output)
index += consumed
column += consumed
continue
} else if char == '"' {
mode = Unquoted
index += 1
column += 1
continue
}
output.write_char(char)
index += 1
column += 1
}
}
}
if mode != Unquoted {
raise UnterminatedQuote(location=quote_start)
}
if active {
finish_lexeme(tokens, output, start, location(path, line_number, column))
}
tokens
}