use std::path::{Path, PathBuf};
use crate::{Config, ConfigError, ConfigResult};
use super::ConfigSource;
#[derive(Debug, Clone)]
pub struct PropertiesConfigSource {
path: PathBuf,
}
impl PropertiesConfigSource {
#[inline]
pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
pub fn parse_content(content: &str) -> Vec<(String, String)> {
let mut result = Vec::new();
let mut lines = content.lines().peekable();
while let Some(line) = lines.next() {
let trimmed = line.trim_start();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('!') {
continue;
}
let mut full_line = trimmed.to_string();
while has_line_continuation(&full_line) {
full_line.pop(); if let Some(next) = lines.next() {
full_line.push_str(next.trim_start());
} else {
break;
}
}
if let Some((key, value)) = parse_key_value(&full_line) {
let key = unescape_properties(key);
let value = unescape_properties(value);
result.push((key, value));
}
}
result
}
}
fn parse_key_value(line: &str) -> Option<(&str, &str)> {
let line = line.trim_start();
for (i, ch) in line.char_indices() {
if ch == '=' || ch == ':' {
if !is_escaped_separator(line, i) {
let value_start = skip_properties_whitespace(line, i + ch.len_utf8());
return Some((&line[..i], &line[value_start..]));
}
}
if ch.is_whitespace() && !is_escaped_separator(line, i) {
let mut value_start = skip_properties_whitespace(line, i);
if let Some((sep, sep_len)) = char_at(line, value_start)
&& (sep == '=' || sep == ':')
&& !is_escaped_separator(line, value_start)
{
value_start = skip_properties_whitespace(line, value_start + sep_len);
}
return Some((&line[..i], &line[value_start..]));
}
}
(!line.is_empty()).then_some((line, ""))
}
#[inline]
fn char_at(line: &str, index: usize) -> Option<(char, usize)> {
if index == line.len() {
return None;
}
let ch = line[index..]
.chars()
.next()
.expect("index below line length should point to a character");
Some((ch, ch.len_utf8()))
}
fn skip_properties_whitespace(line: &str, start: usize) -> usize {
for (offset, ch) in line[start..].char_indices() {
if !ch.is_whitespace() {
return start + offset;
}
}
line.len()
}
#[inline]
fn is_escaped_separator(line: &str, sep_pos: usize) -> bool {
let slash_count = line.as_bytes()[..sep_pos]
.iter()
.rev()
.take_while(|&&b| b == b'\\')
.count();
slash_count % 2 == 1
}
#[inline]
fn has_line_continuation(line: &str) -> bool {
count_trailing_backslashes(line) % 2 == 1
}
#[inline]
fn count_trailing_backslashes(line: &str) -> usize {
line.as_bytes()
.iter()
.rev()
.take_while(|&&b| b == b'\\')
.count()
}
fn unescape_properties(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
let escaped = chars.next().unwrap_or('\\');
match escaped {
'u' => {
let hex: String = chars.by_ref().take(4).collect();
if hex.len() == 4
&& let Ok(code) = u32::from_str_radix(&hex, 16)
&& let Some(unicode_char) = char::from_u32(code)
{
result.push(unicode_char);
continue;
}
result.push('\\');
result.push('u');
result.push_str(&hex);
}
'n' => {
result.push('\n');
}
't' => {
result.push('\t');
}
'r' => {
result.push('\r');
}
'f' => {
result.push('\u{000C}');
}
'\\' => {
result.push('\\');
}
'=' | ':' | ' ' | '#' | '!' => {
result.push(escaped);
}
_ => {
result.push(escaped);
}
}
} else {
result.push(ch);
}
}
result
}
impl ConfigSource for PropertiesConfigSource {
fn load(&self, config: &mut Config) -> ConfigResult<()> {
let content = std::fs::read_to_string(&self.path).map_err(|e| {
ConfigError::IoError(std::io::Error::new(
e.kind(),
format!(
"Failed to read properties file '{}': {}",
self.path.display(),
e
),
))
})?;
for (key, value) in Self::parse_content(&content) {
config.set(&key, value)?;
}
Ok(())
}
}