1use alloc::format;
30use alloc::vec::Vec;
31
32use crate::num::{f64_format, f64_parse, parse_i64};
33
34const FILE_MAX: u64 = 0x100_0000; const LINE_MAX: usize = 0x10000; const LINES_MAX: usize = 0x40000;
37#[cfg(windows)]
38const EOL: &[u8] = b"\r\n";
39#[cfg(not(windows))]
40const EOL: &[u8] = b"\n";
41
42#[derive(Clone, Copy, PartialEq, Eq)]
43enum Kind {
44 Raw, Sec,
46 Kv,
47}
48
49struct Line {
50 kind: Kind,
51 dirty: bool, sec: i32, raw: Vec<u8>, name: Vec<u8>, value: Vec<u8>,
56}
57
58impl Line {
59 fn new(kind: Kind, sec: i32) -> Line {
60 Line { kind, dirty: false, sec, raw: Vec::new(), name: Vec::new(), value: Vec::new() }
61 }
62}
63
64#[derive(Default)]
66pub struct Ini {
67 lines: Vec<Line>,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Item<'a> {
73 Section { line: usize, name: &'a str },
75 Key { line: usize, sec: &'a str, key: &'a str, value: &'a str },
77 Bad { line: usize, text: &'a [u8] },
79}
80
81fn ieq(a: &[u8], b: &[u8]) -> bool {
83 a.len() == b.len()
84 && a.iter().zip(b).all(|(&x, &y)| x == y || (x | 32 == y | 32 && (x | 32).wrapping_sub(b'a') <= 25))
85}
86
87fn blank(c: u8) -> bool {
88 c == b' ' || c == b'\t'
89}
90
91fn name_ok(s: &[u8], is_key: bool) -> bool {
93 let (Some(&first), Some(&last)) = (s.first(), s.last()) else { return false };
94 if blank(first) || blank(last) || (is_key && matches!(first, b';' | b'#' | b'[')) {
95 return false;
96 }
97 let forbid = if is_key { b'=' } else { b']' };
98 !s.iter().any(|&c| c == b'\r' || c == b'\n' || c == 0 || c == forbid)
99}
100
101fn trim(mut s: &[u8]) -> &[u8] {
102 while let [c, rest @ ..] = s {
103 if !blank(*c) {
104 break;
105 }
106 s = rest;
107 }
108 while let [rest @ .., c] = s {
109 if !blank(*c) {
110 break;
111 }
112 s = rest;
113 }
114 s
115}
116
117impl Ini {
118 pub fn new() -> Ini {
119 Ini { lines: Vec::new() }
120 }
121
122 pub fn load(path: &str) -> Ini {
124 let Some(f) = plat::File::open(path, plat::FO_READ) else { return Ini::new() };
125 let size = f.size().unwrap_or(0).min(FILE_MAX) as usize;
126 if size == 0 {
127 return Ini::new();
128 }
129 let mut data = alloc::vec![0u8; size];
130 let got = f.read_at(0, &mut data).unwrap_or(0);
131 drop(f);
132 Ini::parse(&data[..got])
133 }
134
135 pub fn load_checked(path: &str) -> Option<Ini> {
138 let Some(f) = plat::File::open(path, plat::FO_READ) else {
139 return if plat::file_exists(path) == 1 { None } else { Some(Ini::new()) };
140 };
141 let full = f.size().unwrap_or(0);
142 if full > FILE_MAX {
143 return None;
144 }
145 let size = full as usize;
146 if size == 0 {
147 return Some(Ini::new());
148 }
149 let mut data = alloc::vec![0u8; size];
150 let got = f.read_at(0, &mut data)?;
151 drop(f);
152 if got != size {
153 return None;
154 }
155 Some(Ini::parse(&data))
156 }
157
158 pub fn parse(data: &[u8]) -> Ini {
160 let mut ini = Ini::new();
161 let mut pos = if data.starts_with(&[0xEF, 0xBB, 0xBF]) { 3 } else { 0 };
162 let end = data.len();
163 let mut cur: i32 = -1;
164 while pos < end {
165 if ini.lines.len() >= LINES_MAX - 1 {
166 let mut e = end;
168 if data[e - 1] == b'\n' {
169 e -= 1;
170 }
171 while e > pos && data[e - 1] == b'\r' {
172 e -= 1;
173 }
174 ini.classify(&data[pos..e], true, &mut cur);
175 break;
176 }
177 let lf = data[pos..].iter().position(|&c| c == b'\n').map_or(end, |k| pos + k);
178 let nul = data[pos..lf].contains(&0);
179 let mut e = lf;
180 while e > pos && data[e - 1] == b'\r' {
181 e -= 1; }
183 ini.classify(&data[pos..e], nul, &mut cur);
184 pos = lf + 1;
185 }
186 ini
187 }
188
189 fn classify(&mut self, raw: &[u8], verbatim: bool, cur: &mut i32) {
191 let mut ln = Line::new(Kind::Raw, *cur);
192 ln.raw = raw.to_vec();
193 let t = trim(raw);
194 if !verbatim && raw.len() <= LINE_MAX && !t.is_empty() && t[0] != b';' && t[0] != b'#' {
195 if t[0] == b'[' {
196 if let Some(close) = t.iter().position(|&c| c == b']') {
197 let after = trim(&t[close + 1..]);
198 let name = trim(&t[1..close]);
199 if (after.is_empty() || after[0] == b';' || after[0] == b'#') && !name.is_empty() {
200 ln.kind = Kind::Sec;
201 ln.name = name.to_vec();
202 ln.sec = self.lines.len() as i32;
203 *cur = ln.sec; }
205 }
206 } else if let Some(eq) = t.iter().position(|&c| c == b'=') {
207 let key = trim(&t[..eq]);
208 if !key.is_empty() {
209 ln.kind = Kind::Kv;
210 ln.name = key.to_vec();
211 ln.value = trim(&t[eq + 1..]).to_vec();
212 }
213 }
214 }
215 self.lines.push(ln);
216 }
217
218 pub fn to_bytes(&self) -> Vec<u8> {
220 let mut out = Vec::new();
221 for l in &self.lines {
222 if !l.dirty {
223 out.extend_from_slice(&l.raw);
224 } else if l.kind == Kind::Sec {
225 out.push(b'[');
226 out.extend_from_slice(&l.name);
227 out.push(b']');
228 } else {
229 out.extend_from_slice(&l.name);
230 out.push(b'=');
231 out.extend_from_slice(&l.value);
232 }
233 out.extend_from_slice(EOL);
234 }
235 out
236 }
237
238 pub fn save(&self, path: &str) -> bool {
240 let text = self.to_bytes();
241 mkparent(path);
242 plat::write_file(path, &text)
243 }
244
245 fn find_kv(&self, sec: &str, key: &str) -> Option<usize> {
247 let (sec, key) = (sec.as_bytes(), key.as_bytes());
248 self.lines.iter().position(|l| {
249 l.kind == Kind::Kv
250 && ieq(&l.name, key)
251 && if l.sec < 0 { sec.is_empty() } else { !sec.is_empty() && ieq(&self.lines[l.sec as usize].name, sec) }
252 })
253 }
254
255 fn find_sec(&self, sec: &str) -> Option<usize> {
257 self.lines.iter().position(|l| l.kind == Kind::Sec && ieq(&l.name, sec.as_bytes()))
258 }
259
260 fn sec_end(&self, s: i32) -> usize {
263 let mut after = None;
264 let mut first_hdr = None;
265 for (i, l) in self.lines.iter().enumerate() {
266 if l.kind == Kind::Sec {
267 first_hdr.get_or_insert(i);
268 } else if l.kind == Kind::Kv && l.sec == s {
269 after = Some(i + 1);
270 }
271 }
272 match after {
273 Some(a) => a,
274 None if s >= 0 => s as usize + 1,
275 None => first_hdr.unwrap_or(self.lines.len()),
276 }
277 }
278
279 fn line_insert(&mut self, pos: usize, ln: Line) {
281 for l in self.lines.iter_mut() {
282 if l.sec >= pos as i32 {
283 l.sec += 1;
284 }
285 }
286 self.lines.insert(pos, ln);
287 }
288
289 fn line_delete(&mut self, pos: usize) {
291 self.lines.remove(pos);
292 for l in self.lines.iter_mut() {
293 if l.sec == pos as i32 {
294 l.sec = -1;
295 } else if l.sec > pos as i32 {
296 l.sec -= 1;
297 }
298 }
299 }
300
301 pub fn section(&self, sec: &str) -> Vec<(&str, &str)> {
304 let s = sec.as_bytes();
305 self.lines
306 .iter()
307 .filter(|l| l.kind == Kind::Kv && if l.sec < 0 { s.is_empty() } else { !s.is_empty() && ieq(&self.lines[l.sec as usize].name, s) })
308 .filter_map(|l| Some((core::str::from_utf8(&l.name).ok()?, core::str::from_utf8(&l.value).ok()?)))
309 .collect()
310 }
311
312 pub fn sections(&self) -> Vec<&str> {
315 let mut out: Vec<&str> = Vec::new();
316 for l in self.lines.iter().filter(|l| l.kind == Kind::Sec) {
317 if let Ok(n) = core::str::from_utf8(&l.name) {
318 if !out.iter().any(|o| ieq(o.as_bytes(), n.as_bytes())) {
319 out.push(n);
320 }
321 }
322 }
323 out
324 }
325
326 pub fn items(&self) -> Vec<Item<'_>> {
329 let mut out = Vec::new();
330 for (i, l) in self.lines.iter().enumerate() {
331 let line = i + 1;
332 match l.kind {
333 Kind::Sec => match core::str::from_utf8(&l.name) {
334 Ok(name) => out.push(Item::Section { line, name }),
335 Err(_) => out.push(Item::Bad { line, text: &l.raw }),
336 },
337 Kind::Kv => {
338 let sec = if l.sec < 0 { Ok("") } else { core::str::from_utf8(&self.lines[l.sec as usize].name) };
339 match (sec, core::str::from_utf8(&l.name), core::str::from_utf8(&l.value)) {
340 (Ok(sec), Ok(key), Ok(value)) => out.push(Item::Key { line, sec, key, value }),
341 _ => out.push(Item::Bad { line, text: &l.raw }),
342 }
343 }
344 Kind::Raw => {
345 let t = trim(&l.raw);
346 if !t.is_empty() && t[0] != b';' && t[0] != b'#' {
347 out.push(Item::Bad { line, text: &l.raw });
348 }
349 }
350 }
351 }
352 out
353 }
354
355 pub fn get(&self, sec: &str, key: &str) -> Option<&str> {
357 core::str::from_utf8(self.get_bytes(sec, key)?).ok()
358 }
359
360 pub fn get_bytes(&self, sec: &str, key: &str) -> Option<&[u8]> {
361 self.find_kv(sec, key).map(|i| self.lines[i].value.as_slice())
362 }
363
364 pub fn get_int(&self, sec: &str, key: &str, def: i64) -> i64 {
366 self.get_bytes(sec, key).and_then(parse_i64).unwrap_or(def)
367 }
368
369 pub fn get_f64(&self, sec: &str, key: &str, def: f64) -> f64 {
371 match self.get_bytes(sec, key) {
372 Some(v) => match f64_parse(v) {
373 (x, used) if used > 0 && used == v.len() => x,
374 _ => def,
375 },
376 None => def,
377 }
378 }
379
380 pub fn set(&mut self, sec: &str, key: &str, value: &str) -> bool {
382 if !name_ok(key.as_bytes(), true) || (!sec.is_empty() && !name_ok(sec.as_bytes(), false)) {
383 return false;
384 }
385 let v: Vec<u8> = value.bytes().map(|c| if c == b'\r' || c == b'\n' || c == 0 { b' ' } else { c }).collect();
386 if let Some(i) = self.find_kv(sec, key) {
387 let l = &mut self.lines[i];
388 l.value = v;
389 l.dirty = true;
390 return true;
391 }
392 let mut s: i32 = -1; if !sec.is_empty() {
394 s = match self.find_sec(sec) {
395 Some(i) => i as i32,
396 None => {
397 let n = self.lines.len();
399 if n > 0 && !(self.lines[n - 1].kind == Kind::Raw && self.lines[n - 1].raw.is_empty()) {
400 self.line_insert(n, Line::new(Kind::Raw, -1));
401 }
402 let n = self.lines.len();
403 let mut h = Line::new(Kind::Sec, n as i32);
404 h.dirty = true;
405 h.name = sec.as_bytes().to_vec();
406 self.line_insert(n, h);
407 n as i32
408 }
409 };
410 }
411 let pos = self.sec_end(s);
412 let mut k = Line::new(Kind::Kv, s);
413 k.dirty = true;
414 k.name = key.as_bytes().to_vec();
415 k.value = v;
416 self.line_insert(pos, k);
417 true
418 }
419
420 pub fn set_int(&mut self, sec: &str, key: &str, value: i64) -> bool {
422 self.set(sec, key, &format!("{value}"))
423 }
424
425 pub fn set_f64(&mut self, sec: &str, key: &str, value: f64) -> bool {
427 self.set(sec, key, &f64_format(value))
428 }
429
430 pub fn remove(&mut self, sec: &str, key: &str) -> bool {
432 match self.find_kv(sec, key) {
433 Some(i) => {
434 self.line_delete(i);
435 true
436 }
437 None => false,
438 }
439 }
440}
441
442fn mkparent(path: &str) {
444 let b = path.as_bytes();
445 let Some(sep) = b.iter().rposition(|&c| c == b'/' || c == b'\\') else { return };
446 if sep == 0 || (sep == 2 && b[1] == b':') {
447 return; }
449 plat::mkdir(&path[..sep]);
450}