///|
/// Pure config parsing functions extracted from cmd/bit/config.mbt.
/// No IO, no eprint_line, no @sys.exit, no OsFs, no @stdio.
///|
/// Sentinel value for config keys with no value (bare keys without =).
pub fn config_novalue() -> String {
"\u0001"
}
///|
pub fn is_config_novalue(s : String) -> Bool {
s == config_novalue()
}
///|
/// Split config content into logical lines (handling continuation lines).
pub fn config_lines_from_content(content : String) -> Array[String] {
config_preprocess_lines(config_split_lines_preserve_cr(content))
}
///|
/// Split content by newline, preserving \r if present.
fn config_split_lines_preserve_cr(content : String) -> Array[String] {
let lines : Array[String] = []
let mut start = 0
for i, c in content {
if c == '\n' {
lines.push(String::unsafe_substring(content, start~, end=i))
start = i + 1
}
}
if start <= content.length() {
lines.push(String::unsafe_substring(content, start~, end=content.length()))
}
lines
}
///|
/// Join continuation lines (lines ending with unescaped backslash).
fn config_preprocess_lines(raw_lines : Array[String]) -> Array[String] {
let line_info = config_preprocess_lines_with_line_numbers(raw_lines)
let result : Array[String] = []
for info in line_info {
let (line, _line_num) = info
result.push(line)
}
result
}
///|
/// Join continuation lines while keeping their original starting line number.
pub fn config_preprocess_lines_with_line_numbers(
raw_lines : Array[String],
) -> Array[(String, Int)] {
let result : Array[(String, Int)] = []
let mut i = 0
while i < raw_lines.length() {
let mut line = raw_lines[i]
let start_line = i + 1
while config_ends_with_unescaped_backslash(line) &&
config_is_continuation_context(line) &&
i + 1 < raw_lines.length() {
line = String::unsafe_substring(line, start=0, end=line.length() - 1)
i += 1
line = line + raw_lines[i]
}
result.push((line, start_line))
i += 1
}
result
}
///|
fn config_ends_with_unescaped_backslash(s : String) -> Bool {
let len = s.length()
if len == 0 {
return false
}
if s[len - 1] != '\\' {
return false
}
let mut count = 0
let mut idx = len - 1
while idx >= 0 && s[idx] == '\\' {
count += 1
if idx == 0 {
break
}
idx -= 1
}
count % 2 == 1
}
///|
fn config_is_continuation_context(line : String) -> Bool {
let trimmed = config_trim(line)
if trimmed.length() > 0 && (trimmed[0] == '#' || trimmed[0] == ';') {
return false
}
let eq_idx = line.find("=")
match eq_idx {
None => true
Some(ei) => {
let after_eq = String::unsafe_substring(
line,
start=ei + 1,
end=line.length(),
)
!config_has_unquoted_comment_before_backslash(after_eq)
}
}
}
///|
fn config_has_unquoted_comment_before_backslash(value : String) -> Bool {
let mut in_quote = false
let mut last_comment_pos = -1
for i, c in value {
if c == '"' {
let escaped = if i > 0 { value[i - 1] == '\\' } else { false }
if !escaped {
in_quote = !in_quote
}
} else if !in_quote && (c == '#' || c == ';') {
last_comment_pos = i
break
}
}
last_comment_pos >= 0
}
///|
/// Trim whitespace (space, tab, newline, carriage return) from both ends.
fn config_trim(s : String) -> String {
@string_utils.trim_string(s)
}
///|
fn config_trim_chars(s : String, chars : String) -> String {
let mut start = 0
let mut end = s.length()
while start < end {
let c = s.unsafe_get(start)
if config_code_in_string(c, chars) {
start += 1
} else {
break
}
}
while end > start {
let c = s.unsafe_get(end - 1)
if config_code_in_string(c, chars) {
end -= 1
} else {
break
}
}
String::unsafe_substring(s, start~, end~)
}
///|
fn config_code_in_string(c : UInt16, s : String) -> Bool {
for i in 0.. (String, String?, String, Bool)? {
if !line.has_prefix("[") {
return None
}
match line.find("]") {
None => None
Some(close_i) => {
let header = String::unsafe_substring(line, start=1, end=close_i)
let rest = config_trim_chars(
String::unsafe_substring(line, start=close_i + 1, end=line.length()),
" \t",
)
match header.find(" ") {
None =>
match header.find(".") {
Some(dot_i) => {
let sec = String::unsafe_substring(header, start=0, end=dot_i)
let sub = String::unsafe_substring(
header,
start=dot_i + 1,
end=header.length(),
).to_lower()
Some((sec, Some(sub), rest, true))
}
None => Some((header, None, rest, false))
}
Some(space_i) => {
let sec = String::unsafe_substring(header, start=0, end=space_i)
let raw_sub = config_trim_chars(
String::unsafe_substring(
header,
start=space_i + 1,
end=header.length(),
),
" \t",
)
let sub = if raw_sub.length() >= 2 &&
raw_sub[0] == '"' &&
raw_sub[raw_sub.length() - 1] == '"' {
String::unsafe_substring(raw_sub, start=1, end=raw_sub.length() - 1)
} else {
raw_sub
}
Some((sec, Some(sub), rest, false))
}
}
}
}
}
///|
pub fn is_valid_section_name(name : String) -> Bool {
if name.length() == 0 {
return false
}
for c in name {
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-') {
return false
}
}
true
}
///|
/// Compare section header against query for KEY matching.
fn config_section_header_matches(
header_section : String,
header_subsection : String?,
query_section : String,
query_subsection : String?,
) -> Bool {
if header_section.to_lower() != query_section.to_lower() {
return false
}
match (header_subsection, query_subsection) {
(None, None) => true
(Some(hs), Some(qs)) => hs == qs
_ => false
}
}
///|
/// Compare section header against query for SECTION matching (for insert).
/// For dot-form headers, uses case-insensitive matching for the subsection.
/// For quoted headers, uses case-sensitive subsection matching.
pub fn section_matches_for_insert(
header_section : String,
header_subsection : String?,
query_section : String,
query_subsection : String?,
is_dot_form : Bool,
) -> Bool {
if header_section.to_lower() != query_section.to_lower() {
return false
}
match (header_subsection, query_subsection) {
(None, None) => true
(Some(hs), Some(qs)) =>
if is_dot_form {
hs.to_lower() == qs.to_lower()
} else {
hs == qs
}
_ => false
}
}
///|
/// Parse a config key like "section.key" or "section.subsection.key".
/// Returns (section, subsection?, key_name) or None.
/// Validates that the key name starts with an alphabetic character (git compat).
pub fn parse_config_key(key : String) -> (String, String?, String)? {
if key.contains("\n") {
return None
}
let parts : Array[String] = []
for p in key.split(".") {
parts.push(p.to_owned())
}
if parts.length() < 2 {
return None
}
if parts[0].length() == 0 {
return None
}
let key_name = parts[parts.length() - 1]
if key_name.length() == 0 {
return None
}
let first = key_name[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')) {
return None
}
if parts.length() == 2 {
Some((parts[0], None, key_name))
} else {
let sub : Array[String] = []
for i in 1..<(parts.length() - 1) {
sub.push(parts[i])
}
Some((parts[0], Some(sub.join(".")), key_name))
}
}
///|
/// Find the line number (1-based) of a config key in content.
pub fn find_config_key_line_from_content(
content : String,
key : String,
) -> Int? {
let target = parse_config_key(key)
guard target is Some((sec, sub, name)) else { return None }
let sec_l = sec.to_lower()
let name_l = name.to_lower()
let lines = config_split_lines_preserve_cr(content)
let mut current_section = ""
let mut current_subsection : String? = None
for i, line in lines {
let trimmed = config_trim(line)
if trimmed.length() == 0 || trimmed[0] == '#' || trimmed[0] == ';' {
continue
}
match parse_section_header_line(trimmed) {
Some((hs, hsub, rest, _)) => {
current_section = hs.to_lower()
current_subsection = hsub
if config_section_header_matches(
current_section, current_subsection, sec_l, sub,
) &&
rest.length() > 0 {
let eq = rest.find("=")
let key_part = match eq {
Some(ei) =>
config_trim_chars(
String::unsafe_substring(rest, start=0, end=ei),
" \t",
)
None => config_trim_chars(rest, " \t")
}
if key_part.to_lower() == name_l {
return Some(i + 1)
}
}
}
None => {
if !config_section_header_matches(
current_section, current_subsection, sec_l, sub,
) {
continue
}
let eq = trimmed.find("=")
let key_part = match eq {
Some(ei) =>
config_trim_chars(
String::unsafe_substring(trimmed, start=0, end=ei),
" \t",
)
None => config_trim_chars(trimmed, " \t")
}
if key_part.to_lower() == name_l {
return Some(i + 1)
}
}
}
}
None
}
///|
/// Extract a config value for a given key from content string.
/// Returns the last matching value (git semantics: last wins).
pub fn get_config_value_from_content(
content : String,
key : String,
value_pattern : String?,
fixed_value : Bool,
) -> String? {
let lines = config_lines_from_content(content)
guard parse_config_key(key) is Some((section, subsection, name)) else {
return None
}
let name_l = name.to_lower()
let mut in_section = false
let mut found : String? = None
for line in lines {
let trimmed = config_trim(line)
if trimmed.length() == 0 || trimmed[0] == '#' || trimmed[0] == ';' {
continue
}
match parse_section_header_line(trimmed) {
Some((hs, hsub, rest, _dot_form)) => {
in_section = config_section_header_matches(
hs, hsub, section, subsection,
)
if in_section && rest.length() > 0 {
let eq_idx = rest.find("=")
match eq_idx {
Some(ei) => {
let key_part = config_trim_chars(
String::unsafe_substring(rest, start=0, end=ei),
" \t",
)
if key_part.to_lower() == name_l {
let val = parse_config_value(
String::unsafe_substring(
rest,
start=ei + 1,
end=rest.length(),
),
)
if config_matches_value_filter(val, value_pattern, fixed_value) {
found = Some(val)
}
}
}
None => {
let key_part = config_trim_chars(rest, " \t")
if key_part.to_lower() == name_l {
let val = config_novalue()
if config_matches_value_filter("", value_pattern, fixed_value) {
found = Some(val)
}
}
}
}
}
}
None =>
if in_section {
let eq_idx = trimmed.find("=")
match eq_idx {
Some(i) => {
let key_part = config_trim_chars(
String::unsafe_substring(trimmed, start=0, end=i),
" \t",
)
if key_part.to_lower() == name_l {
let raw_val = match line.find("=") {
Some(raw_i) =>
String::unsafe_substring(
line,
start=raw_i + 1,
end=line.length(),
)
None =>
String::unsafe_substring(
trimmed,
start=i + 1,
end=trimmed.length(),
)
}
let val = parse_config_value(raw_val)
if config_matches_value_filter(val, value_pattern, fixed_value) {
found = Some(val)
}
}
}
None => {
let key_part = config_trim_chars(trimmed, " \t")
if key_part.to_lower() == name_l {
let val = config_novalue()
if config_matches_value_filter("", value_pattern, fixed_value) {
found = Some(val)
}
}
}
}
}
}
}
found
}
///|
/// Extract all values for a given key from content string.
pub fn get_all_config_values_from_content(
content : String,
key : String,
value_pattern : String?,
fixed_value : Bool,
) -> Array[String] {
let lines = config_lines_from_content(content)
guard parse_config_key(key) is Some((section, subsection, name)) else {
return []
}
let name_l = name.to_lower()
let mut in_section = false
let result : Array[String] = []
for line in lines {
let trimmed = config_trim(line)
if trimmed.length() == 0 || trimmed[0] == '#' || trimmed[0] == ';' {
continue
}
match parse_section_header_line(trimmed) {
Some((hs, hsub, rest, _dot_form)) => {
in_section = config_section_header_matches(
hs, hsub, section, subsection,
)
if in_section && rest.length() > 0 {
let eq_idx = rest.find("=")
match eq_idx {
Some(ei) => {
let key_part = config_trim_chars(
String::unsafe_substring(rest, start=0, end=ei),
" \t",
)
if key_part.to_lower() == name_l {
let val = parse_config_value(
String::unsafe_substring(
rest,
start=ei + 1,
end=rest.length(),
),
)
if config_matches_value_filter(val, value_pattern, fixed_value) {
result.push(val)
}
}
}
None => {
let key_part = config_trim_chars(rest, " \t")
if key_part.to_lower() == name_l {
if config_matches_value_filter("", value_pattern, fixed_value) {
result.push(config_novalue())
}
}
}
}
}
}
None =>
if in_section {
let eq_idx = trimmed.find("=")
match eq_idx {
Some(i) => {
let key_part = config_trim_chars(
String::unsafe_substring(trimmed, start=0, end=i),
" \t",
)
if key_part.to_lower() == name_l {
let raw_val = match line.find("=") {
Some(raw_i) =>
String::unsafe_substring(
line,
start=raw_i + 1,
end=line.length(),
)
None =>
String::unsafe_substring(
trimmed,
start=i + 1,
end=trimmed.length(),
)
}
let val = parse_config_value(raw_val)
if config_matches_value_filter(val, value_pattern, fixed_value) {
result.push(val)
}
}
}
None => {
let key_part = config_trim_chars(trimmed, " \t")
if key_part.to_lower() == name_l {
if config_matches_value_filter("", value_pattern, fixed_value) {
result.push(config_novalue())
}
}
}
}
}
}
}
result
}
///|
fn config_matches_value_filter(
val : String,
pattern : String?,
fixed_value : Bool,
) -> Bool {
match pattern {
None => true
Some(p) =>
if fixed_value {
val == p
} else {
config_value_pattern_matches(val, p)
}
}
}
///|
fn config_is_horizontal_space(c : Char) -> Bool {
c == ' ' || c == '\t'
}
///|
fn config_decode_escape(c : Char) -> Char {
match c {
'n' => '\n'
't' => '\t'
'r' => '\r'
_ => c
}
}
///|
/// Parse a quoted/escaped config value string.
pub fn parse_config_value(raw : String) -> String {
let sb = StringBuilder::new()
let mut in_single = false
let mut in_double = false
let mut escape = false
let mut started = false
let mut trailing_ws = 0
for c in raw {
if escape {
sb.write_char(config_decode_escape(c))
started = true
trailing_ws = 0
escape = false
continue
}
if in_double {
if c == '\\' {
escape = true
continue
}
if c == '"' {
in_double = false
started = true
trailing_ws = 0
continue
}
sb.write_char(c)
started = true
trailing_ws = 0
continue
}
if in_single {
if c == '\'' {
in_single = false
started = true
trailing_ws = 0
continue
}
sb.write_char(c)
started = true
trailing_ws = 0
continue
}
if !started && config_is_horizontal_space(c) {
continue
}
if c == '"' {
in_double = true
started = true
continue
}
if c == '\'' {
in_single = true
started = true
continue
}
if c == '\\' {
escape = true
started = true
trailing_ws = 0
continue
}
if c == '#' || c == ';' {
if !started || trailing_ws > 0 {
break
}
}
sb.write_char(c)
started = true
if config_is_horizontal_space(c) {
trailing_ws += 1
} else {
trailing_ws = 0
}
}
let parsed = sb.to_string()
if trailing_ws > 0 && parsed.length() >= trailing_ws {
String::unsafe_substring(parsed, start=0, end=parsed.length() - trailing_ws)
} else {
parsed
}
}
///|
/// Encode a value for writing to a config file, adding
/// quoting when needed (leading/trailing space, #, ;).
pub fn encode_config_value(value : String) -> String {
let len = value.length()
let needs_quoting = len > 0 &&
(
value[0] == ' ' ||
value[0] == '\t' ||
value[len - 1] == ' ' ||
value[len - 1] == '\t' ||
value.contains("#") ||
value.contains(";")
)
let sb = StringBuilder::new()
if needs_quoting {
sb.write_char('"')
}
for c in value {
match c {
'\n' => sb.write_string("\\n")
'\t' => sb.write_string("\\t")
'\r' => sb.write_string("\\r")
'"' => sb.write_string("\\\"")
'\\' => sb.write_string("\\\\")
_ => sb.write_char(c)
}
}
if needs_quoting {
sb.write_char('"')
}
sb.to_string()
}
///|
/// Parse boolean keyword values: true/false/yes/no/on/off.
pub fn parse_bool_keyword_value(value : String) -> Bool? {
let trimmed = value.trim().to_owned().to_lower()
match trimmed {
"true" | "yes" | "on" | "1" => Some(true)
"false" | "no" | "off" | "0" | "" => Some(false)
_ => None
}
}
///|
/// Parse an integer config value with optional k/m/g suffix.
pub fn parse_config_int_value(val : String) -> (Int64?, String?) {
match config_parse_size_value(val) {
Some(n) => (Some(n), (None : String?))
None => {
let trimmed = val.trim().to_owned().to_lower()
if trimmed.length() == 0 {
return ((None : Int64?), (None : String?))
}
let last = trimmed[trimmed.length() - 1]
let invalid_unit = !(last >= '0' && last <= '9') &&
last != 'k' &&
last != 'm' &&
last != 'g'
if invalid_unit {
((None : Int64?), Some("invalid unit"))
} else {
((None : Int64?), (None : String?))
}
}
}
}
///|
/// Parse a size value with optional k/m/g suffix.
pub fn config_parse_size_value(value : String) -> Int64? {
let trimmed = value.trim().to_owned().to_lower()
if trimmed.length() == 0 {
return None
}
let len = trimmed.length()
let last_char = trimmed[len - 1]
let (num_str, multiplier) : (String, Int64) = if last_char >= '0' &&
last_char <= '9' {
(trimmed, 1L)
} else {
let num_part = String::unsafe_substring(trimmed, start=0, end=len - 1)
let mult : Int64 = match last_char {
'k' => 1024L
'm' => 1024L * 1024L
'g' => 1024L * 1024L * 1024L
_ => return None
}
(num_part, mult)
}
let num = @string.parse_int64(num_str) catch { _ => return None }
Some(num * multiplier)
}
// --- Regex helpers for config value pattern matching ---
///|
/// Match a value against a pattern, supporting ! prefix for negation.
fn config_value_pattern_matches(text : String, pattern : String) -> Bool {
let negate = pattern.length() > 0 && pattern[0] == '!'
let actual = if negate {
String::unsafe_substring(pattern, start=1, end=pattern.length())
} else {
pattern
}
let result = config_regex_matches(text, actual)
if negate {
!result
} else {
result
}
}
///|
fn config_regex_matches(text : String, pattern : String) -> Bool {
let tc = text.to_array()
let pc = pattern.to_array()
if pc.length() == 0 {
return true
}
let anchored_start = pc[0] == '^'
let pi_start = if anchored_start { 1 } else { 0 }
if anchored_start {
return config_regex_match_here(tc, 0, pc, pi_start)
}
let mut i = 0
while i <= tc.length() {
if config_regex_match_here(tc, i, pc, pi_start) {
return true
}
i += 1
}
false
}
///|
fn config_regex_match_here(
tc : Array[Char],
ti : Int,
pc : Array[Char],
pi : Int,
) -> Bool {
if pi >= pc.length() {
return true
}
if pc[pi] == '$' && pi + 1 >= pc.length() {
return ti >= tc.length()
}
let atom_end = config_regex_atom_end(pc, pi)
if atom_end < pc.length() {
let q = pc[atom_end]
if q == '*' {
return config_regex_match_star(tc, ti, pc, pi, atom_end, atom_end + 1)
}
if q == '+' {
if ti < tc.length() && config_regex_atom_matches_char(pc, pi, tc[ti]) {
return config_regex_match_star(
tc,
ti + 1,
pc,
pi,
atom_end,
atom_end + 1,
)
}
return false
}
if q == '?' {
if ti < tc.length() && config_regex_atom_matches_char(pc, pi, tc[ti]) {
if config_regex_match_here(tc, ti + 1, pc, atom_end + 1) {
return true
}
}
return config_regex_match_here(tc, ti, pc, atom_end + 1)
}
}
if ti < tc.length() && config_regex_atom_matches_char(pc, pi, tc[ti]) {
return config_regex_match_here(tc, ti + 1, pc, atom_end)
}
false
}
///|
fn config_regex_match_star(
tc : Array[Char],
ti : Int,
pc : Array[Char],
atom_pi : Int,
_atom_end : Int,
rest_pi : Int,
) -> Bool {
let mut max_ti = ti
while max_ti < tc.length() &&
config_regex_atom_matches_char(pc, atom_pi, tc[max_ti]) {
max_ti += 1
}
let mut t = max_ti
while t >= ti {
if config_regex_match_here(tc, t, pc, rest_pi) {
return true
}
t -= 1
}
false
}
///|
fn config_regex_atom_end(pc : Array[Char], pi : Int) -> Int {
if pc[pi] == '\\' && pi + 1 < pc.length() {
pi + 2
} else if pc[pi] == '[' {
let mut i = pi + 1
if i < pc.length() && pc[i] == '^' {
i += 1
}
if i < pc.length() && pc[i] == ']' {
i += 1
}
while i < pc.length() && pc[i] != ']' {
i += 1
}
if i < pc.length() {
i + 1
} else {
pc.length()
}
} else {
pi + 1
}
}
///|
fn config_regex_atom_matches_char(pc : Array[Char], pi : Int, c : Char) -> Bool {
if pc[pi] == '.' {
true
} else if pc[pi] == '\\' && pi + 1 < pc.length() {
c == pc[pi + 1]
} else if pc[pi] == '[' {
config_regex_char_class_matches(pc, pi, c)
} else {
c == pc[pi]
}
}
///|
fn config_regex_char_class_matches(
pc : Array[Char],
pi : Int,
c : Char,
) -> Bool {
let mut i = pi + 1
let negate = i < pc.length() && pc[i] == '^'
if negate {
i += 1
}
let class_start = i
let mut matched = false
while i < pc.length() {
if pc[i] == ']' && i > class_start {
break
}
if i + 2 < pc.length() && pc[i + 1] == '-' && pc[i + 2] != ']' {
if c >= pc[i] && c <= pc[i + 2] {
matched = true
}
i += 3
continue
}
if c == pc[i] {
matched = true
}
i += 1
}
if negate {
!matched
} else {
matched
}
}