use ascii; use bufio; use encoding::utf8; use errors; use io; use locale; use memio; use strings; export type scanner = struct { scanner: bufio::scanner, group: str, }; // 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), ... }; // Returns the next line from a desktop entry file. The return value is // 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]]. export fn next(this: *scanner) (line | io::EOF | error) = { let text = match (bufio::scan_line(&this.scanner)) { case let text: const str => yield text; case let err: (io::error | utf8::invalid) => return err; case io::EOF => return io::EOF; }; if (text == "") { // blank line return blank; } else if (strings::hasprefix(text, '#')) { // comment return strings::ltrim(text, '#'): comment; } else if (strings::hasprefix(text, '[')) { // group header let header = parse_group_header(text)?; free(this.group); this.group = strings::dup(header: str); return header; } else { // key/value pair let entry = parse_entry(text)?; entry.group = this.group; return entry; }; }; // 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; }; }; // Frees resources associated with a [[scanner]]. export fn finish(this: *scanner) void = { bufio::finish(&this.scanner); free(this.group); }; // memory is borrowed from the input fn parse_group_header(text: str) (group_header | error) = { if (!strings::hasprefix(text, '[') || !strings::hassuffix(text, ']')) { return invalid_group_header; }; text = strings::rtrim(strings::ltrim(text, '['), ']'); if (strings::contains(text, '[', ']')) { return invalid_group_header; }; return text: group_header; }; // memory is borrowed from the input fn parse_entry(line: str) (entry | error) = { if (!strings::contains(line, '=')) return invalid_entry; let (key, valu) = strings::cut(line, "="); key = strings::ltrim(strings::rtrim(key)); let (key, local_string) = strings::cut(key, "["); let local = if (local_string == "") { yield locale::c; } else { local_string = strings::rtrim(local_string, ']'); validate_entry_locale(local_string)?; yield match(locale::parse(local_string)) { case let local: locale::locale => yield local; case locale::invalid => return invalid_entry; }; }; validate_entry_key(key)?; return entry { key = key, value = valu, locale = local, ... }; }; 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; }; }; 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; }; };