2024-10-20 16:40:35 -06:00
|
|
|
use ascii;
|
2024-10-12 20:12:43 -06:00
|
|
|
use bufio;
|
2024-10-12 21:34:57 -06:00
|
|
|
use encoding::utf8;
|
2024-10-12 21:43:07 -06:00
|
|
|
use errors;
|
2024-10-12 20:12:43 -06:00
|
|
|
use io;
|
2024-10-12 21:34:57 -06:00
|
|
|
use locale;
|
2024-10-12 20:12:43 -06:00
|
|
|
use memio;
|
2024-10-12 21:34:57 -06:00
|
|
|
use strings;
|
2024-10-12 20:12:43 -06:00
|
|
|
|
2024-10-19 13:23:28 -06:00
|
|
|
export type scanner = struct {
|
2024-10-19 14:15:23 -06:00
|
|
|
scanner: bufio::scanner,
|
|
|
|
group: str,
|
2024-10-19 13:23:28 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
// Creates a desktop entry file scanner. Use [[next]] to read lines. The caller
|
|
|
|
// must call [[finish]] once they're done with this object.
|
|
|
|
export fn scan(input: io::handle) scanner = scanner {
|
|
|
|
scanner = bufio::newscanner(input),
|
|
|
|
...
|
|
|
|
};
|
2024-10-12 20:12:43 -06:00
|
|
|
|
2024-10-19 14:23:13 -06:00
|
|
|
// Returns the next line from a desktop entry file. The return value is
|
2024-10-20 11:03:19 -06:00
|
|
|
// borrowed from the [[scanner]]. Use [[strings::dup]], [[entry_dup]], etc. to
|
|
|
|
// retain a copy. If all you want is the file's data, use [[next_entry]].
|
2024-10-19 13:23:28 -06:00
|
|
|
export fn next(this: *scanner) (line | io::EOF | error) = {
|
2024-10-21 10:40:31 -06:00
|
|
|
let text = match (bufio::scan_line(&this.scanner)?) {
|
|
|
|
case let text: const str => yield text;
|
|
|
|
case io::EOF => return io::EOF;
|
2024-10-19 13:23:28 -06:00
|
|
|
};
|
2024-10-20 16:40:35 -06:00
|
|
|
|
2024-10-19 13:23:28 -06:00
|
|
|
if (text == "") {
|
|
|
|
// blank line
|
|
|
|
return blank;
|
|
|
|
|
|
|
|
} else if (strings::hasprefix(text, '#')) {
|
|
|
|
// comment
|
|
|
|
return strings::ltrim(text, '#'): comment;
|
|
|
|
|
|
|
|
} else if (strings::hasprefix(text, '[')) {
|
|
|
|
// group header
|
2024-10-19 14:15:23 -06:00
|
|
|
let header = parse_group_header(text)?;
|
|
|
|
free(this.group);
|
2024-10-20 16:40:35 -06:00
|
|
|
this.group = strings::dup(header: str);
|
2024-10-19 14:15:23 -06:00
|
|
|
return header;
|
2024-10-12 20:12:43 -06:00
|
|
|
|
2024-10-19 13:23:28 -06:00
|
|
|
} else {
|
|
|
|
// key/value pair
|
2024-10-19 14:15:23 -06:00
|
|
|
let entry = parse_entry(text)?;
|
|
|
|
entry.group = this.group;
|
|
|
|
return entry;
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
2024-10-12 21:43:07 -06:00
|
|
|
};
|
|
|
|
|
2024-10-19 14:23:13 -06:00
|
|
|
// Returns the next entry from the desktop file, skipping over blank lines,
|
|
|
|
// comments, group headers, etc. The return value is borrowed from the
|
|
|
|
// [[scanner]]. Use [[entry_dup]] to retain a copy.
|
|
|
|
export fn next_entry(this: *scanner) (entry | io::EOF | error) = {
|
|
|
|
for (true) match(next(this)?) {
|
|
|
|
case let entr: entry => return entr;
|
|
|
|
case io::EOF => return io::EOF;
|
|
|
|
case => void;
|
|
|
|
};
|
|
|
|
};
|
2024-10-19 14:15:23 -06:00
|
|
|
|
2024-10-19 13:23:28 -06:00
|
|
|
// Frees resources associated with a [[scanner]].
|
|
|
|
export fn finish(this: *scanner) void = {
|
|
|
|
bufio::finish(&this.scanner);
|
2024-10-19 14:15:23 -06:00
|
|
|
free(this.group);
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
// memory is borrowed from the input
|
2024-10-19 13:23:28 -06:00
|
|
|
fn parse_group_header(text: str) (group_header | error) = {
|
2024-10-12 21:34:57 -06:00
|
|
|
if (!strings::hasprefix(text, '[') || !strings::hassuffix(text, ']')) {
|
|
|
|
return invalid_group_header;
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
text = strings::rtrim(strings::ltrim(text, '['), ']');
|
2024-10-12 21:34:57 -06:00
|
|
|
if (strings::contains(text, '[', ']')) {
|
|
|
|
return invalid_group_header;
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
2024-10-19 13:23:28 -06:00
|
|
|
return text: group_header;
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
|
|
|
|
2024-10-19 14:15:23 -06:00
|
|
|
// memory is borrowed from the input
|
|
|
|
fn parse_entry(line: str) (entry | error) = {
|
2024-10-12 21:34:57 -06:00
|
|
|
if (!strings::contains(line, '=')) return invalid_entry;
|
|
|
|
let (key, valu) = strings::cut(line, "=");
|
2024-10-12 20:12:43 -06:00
|
|
|
key = strings::ltrim(strings::rtrim(key));
|
|
|
|
|
2024-10-12 21:34:57 -06:00
|
|
|
let (key, local_string) = strings::cut(key, "[");
|
2024-10-20 16:40:35 -06:00
|
|
|
let local = if (local_string == "") {
|
2024-10-19 14:15:23 -06:00
|
|
|
yield locale::c;
|
|
|
|
} else {
|
2024-10-12 21:43:07 -06:00
|
|
|
local_string = strings::rtrim(local_string, ']');
|
2024-10-20 16:40:35 -06:00
|
|
|
validate_entry_locale(local_string)?;
|
|
|
|
|
2024-10-19 14:15:23 -06:00
|
|
|
yield match(locale::parse(local_string)) {
|
2024-10-12 21:34:57 -06:00
|
|
|
case let local: locale::locale => yield local;
|
2024-10-12 21:51:52 -06:00
|
|
|
case locale::invalid => return invalid_entry;
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
|
|
|
};
|
2024-10-22 15:27:14 -06:00
|
|
|
local.encoding = "";
|
2024-10-12 20:12:43 -06:00
|
|
|
|
2024-10-20 16:40:35 -06:00
|
|
|
validate_entry_key(key)?;
|
2024-10-19 14:15:23 -06:00
|
|
|
return entry {
|
|
|
|
key = key,
|
|
|
|
value = valu,
|
|
|
|
locale = local,
|
|
|
|
...
|
|
|
|
};
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
|
|
|
|
2024-10-20 16:40:35 -06:00
|
|
|
fn validate_entry_key(key: str) (void | error) = {
|
|
|
|
for (let byte .. strings::toutf8(key)) {
|
|
|
|
const ok =
|
|
|
|
(byte >= 'A' && byte <= 'Z') ||
|
|
|
|
(byte >= 'a' && byte <= 'z') ||
|
|
|
|
(byte >= '0' && byte <= '9') ||
|
|
|
|
(byte == '-');
|
|
|
|
if (!ok) return invalid_entry;
|
|
|
|
};
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|
|
|
|
|
2024-10-20 16:40:35 -06:00
|
|
|
fn validate_entry_locale(locale: str) (void | error) = {
|
|
|
|
for (let byte .. strings::toutf8(locale)) {
|
|
|
|
const bad =
|
|
|
|
(byte == '[') ||
|
|
|
|
(byte == ']') ||
|
|
|
|
(byte == '=') ||
|
|
|
|
!ascii::isprint(byte: rune);
|
|
|
|
if (bad) return invalid_entry;
|
|
|
|
};
|
2024-10-12 20:12:43 -06:00
|
|
|
};
|