1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use std::fs;
use std::path::{Path, PathBuf};
pub struct Config {
// [settings]
pub minimal: Option<bool>,
pub no_asn: Option<bool>,
// [headers]
pub http_headers: Vec<String>,
}
#[derive(PartialEq)]
enum Section {
Settings,
Headers,
}
impl Config {
pub fn default_path() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
.unwrap_or_else(|| PathBuf::from("."))
.join("meowping.conf")
}
/// Parses a .conf file with optional INI-style sections.
///
/// ```text
/// [settings]
/// minimal = true
/// no_asn = false
///
/// [headers]
/// User-Agent: curl/8.0
/// Accept: */*
/// ```
///
/// Lines before any section header, or under `[headers]`, are treated as
/// raw HTTP header lines (`Name: value`). Lines under `[settings]` are
/// `key = value` pairs. Blank lines and `#` comments are ignored.
pub fn load(path: &Path) -> Result<Self, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("Failed to read config file '{}': {}", path.display(), e))?;
let mut minimal = None;
let mut no_asn = None;
let mut http_headers = Vec::new();
let mut section = Section::Headers;
for (i, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.eq_ignore_ascii_case("[settings]") {
section = Section::Settings;
continue;
}
if line.eq_ignore_ascii_case("[headers]") {
section = Section::Headers;
continue;
}
match section {
Section::Settings => {
let (key, value) = line.split_once('=').ok_or_else(|| {
format!(
"Config line {}: expected 'key = value', got: {}",
i + 1,
line
)
})?;
match key.trim() {
"minimal" => minimal = Some(parse_bool(value.trim(), i + 1)?),
"no_asn" => no_asn = Some(parse_bool(value.trim(), i + 1)?),
unknown => {
return Err(format!(
"Config line {}: unknown setting '{}'",
i + 1,
unknown
));
}
}
}
Section::Headers => {
if !line.contains(':') {
return Err(format!(
"Config line {}: expected 'Header-Name: value', got: {}",
i + 1,
line
));
}
http_headers.push(line.to_string());
}
}
}
Ok(Self {
minimal,
no_asn,
http_headers,
})
}
}
fn parse_bool(s: &str, line: usize) -> Result<bool, String> {
match s {
"true" | "1" | "yes" => Ok(true),
"false" | "0" | "no" => Ok(false),
_ => Err(format!("Config line {line}: expected true/false, got: {s}")),
}
}