diff --git a/format/desktop_entry/desktop_entry.ha b/format/desktop_entry/desktop_entry.ha index a438f49..f2b006e 100644 --- a/format/desktop_entry/desktop_entry.ha +++ b/format/desktop_entry/desktop_entry.ha @@ -17,7 +17,11 @@ export type group = struct { // A line in the file, which can be a comment (or a blank line), an entry, or // a localized entry. -export type line = (comment | entry | localized_entry); +export type line = (blank | comment | entry | localized_entry); + +// A blank line. +// Specification: §3.1 +export type blank = void; // A comment. // Specification: §3.1 @@ -38,13 +42,8 @@ export type localized_entry = struct { locale: locale::locale, }; -// An entry value. Values that reference memory (such as [[str]]) are borrowed -// from the [[file]]. These may be free'd or overwritten when other functions in -// this module are called, so [[value_dup]] is required to extend its lifetime. -export type value = (str | bool | f32); - -// Gets a non-localized value from a [[file]]. If the group does not exist, or -// the group exists but the key isn't in it, it returns void. +// Gets a non-localized [[value]] from a [[file]]. If the group does not exist, +// or the group exists but the key isn't in it, it returns void. export fn file_get(fil: *file, group_name: str, key: str) (value | void) = { let grou = match (file_find_group(fil, group_name)) { case let index: size => yield &fil.groups[index]; @@ -91,6 +90,8 @@ export fn file_get_localized( }; }; +// TODO + //export fn file_add //export fn file_add_localized @@ -101,7 +102,17 @@ export fn file_get_localized( //export fn file_encode -//export fn file_finish +// Frees memory associated with a [[file]]. +export fn file_finish(fil *file) void = { + for (let lin .. file.lines) { + line_finish(lin); + }; + free(file.lines); + for (let grou .. file.groups) { + group_finish(grou); + }; + free(file.groups); +}; fn file_find_group(fil: *file, group_name: str) (size | void) = { let index = 0z; @@ -173,7 +184,7 @@ fn group_find_localized_entry(grou: *group, key: str, local: locale::locale) (si return void; }; -fn group_find_localized_entry_exact(grou: *group, key: str, local: locale::locale) (size | void) = { +fn group_find_localized_entry_exact(grou: group, key: str, local: locale::locale) (size | void) = { let index = 0z; for (let lin .. grou.lines) { match (lin) { @@ -186,15 +197,3 @@ fn group_find_localized_entry_exact(grou: *group, key: str, local: locale::local index += 1; }; }; - -// Duplicates a [[value]]. Use [[value_finish]] to get rid of it. -export fn value_dup(valu: value) value = match (valu) { -case let valu: str => yield strings::dup(valu); -case => yield valu; -}; - -// Frees an [[entry]] previously duplicated with [[entry_dup]]. -export fn value_finish(valu: value) void = match (valu) { -case let valu: str => free(valu); -case => void; -}; diff --git a/format/desktop_entry/error.ha b/format/desktop_entry/error.ha new file mode 100644 index 0000000..f3c2b73 --- /dev/null +++ b/format/desktop_entry/error.ha @@ -0,0 +1,28 @@ +// Any error which may be returned from a function in this module. +export type error = !enum { + INVALID_GROUP_HEADER, + INVALID_ENTRY, + // unused for now + DUPLICATE_GROUP, + DUPLICATE_ENTRY, + DUPLICATE_LOCALIZATION, + NO_DEFAULT_VALUE, + ENTRY_OUTSIDE_GROUP, + // -------------- + UNSUPPORTED_ESCAPE, + STRING_NOT_ASCII, +}; + +// Converts a desktop entry [[error]] into a user-friendly string. +export fn strerror(err: error) str = switch { +case INVALID_GROUP_HEADER => yield "invalid group header"; +case INVALID_ENTRY => yield "invalid entry"; +case DUPLICATE_GROUP => yield "duplicate group"; +case DUPLICATE_ENTRY => yield "duplicate entry"; +case DUPLICATE_LOCALIZATION => yield "duplicate localization"; +case NO_DEFAULT_VALUE => yield "no default value"; +case ENTRY_OUTSIDE_GROUP => yield "entry outside group"; +case UNSUPPORTED_ESCAPE => yield "unsupported escape"; +case STRING_NOT_ASCII => yield "string not ascii"; +case => yield "unknown"; +}; diff --git a/format/desktop_entry/parse.ha b/format/desktop_entry/parse.ha new file mode 100644 index 0000000..eced0a0 --- /dev/null +++ b/format/desktop_entry/parse.ha @@ -0,0 +1,115 @@ +use encoding; +use bufio; +use io; +use memio; + +// Parses a [[file]]. The result must be freed using [[file_finish]]. +export fn file_parse(in: io::stream) (file | error | io::error | utf8::invalid) = { + let scanner = bufio::newscanner(in); + defer bufio::finish(scanner); + + let fil: file = { ... }; + + for (true) { + let text = match (bufio::scan_line(&scanner)) { + case let text: const str => + yield text; + case let err: io::EOF => + break; + case let err: (io::error | utf8::invalid) => + file_finish(fil); + return err; + }; + + if (text == "") { + // blank line + file_append_line(fil, blank); + + } else if (strings::has_prefix(text, '#')) { + // comment + let text = strings::dup(strings::ltrim(text, '#')); + file_append_line(fil, text: comment); + + } else if (strings::has_prefix(text, '[')) { + // group header + let name = strings::dup(parse_group_header(text)?); + file_append_group(fil, name); + + } else { + // key/value pair + let (key, valu, local) = match (parse_entry()) { + case let result: (str, value, (locale::locale | void)) => + yield result; + case let err: error => + file_finish(fil); + return err; + }; + + file_append_line(fil, match (local) { + case let local: locale::locale => + yield localized_entry { + key = key, + value = valu, + locale = local, + }; + case void => + yield entry { + key = key, + value = valu, + }; + }); + }; + }; + return fil; +}; + +fn file_append_line(fil: *file, lin: line) = { + if (len(fil.groups) > 0) { + append(fil.groups[len(fil.groups - 1)].lines, lin); + } else { + append(fil.preceeding_lines, lin); + }; +}; + +// memory is borrowed from the input +fn parse_group_header(text: str) (str | error) = { + if !strings::has_prefix(text, '[') || !strings::has_suffix(text, ']') { + return error::INVALID_GROUP_HEADER; + }; + + text = strings::rtrim(strings::ltrim(text, '['), ']'); + if strings::contains(text, '[', ']') { + return error::INVALID_GROUP_HEADER; + }; + return text; +}; + +// memory must be freed by the caller +fn parse_entry(line: str) ((str, str, (locale::locale | void)) | error) = { + if !strings::contains(line, '=') return error::INVALID_ENTRY; + let key, valu_string = strings::cut(line, '='); + key = strings::ltrim(strings::rtrim(key)); + if !validate_entry_key(key) return error::INVALID_ENTRY; + + let local = (locale::locale | void) = void; + let (key, local_string) = strings::cut(key, '['); + if (local_string != "") { + local_string = locale(strings::rtrim(local, ']')); + if (!validate_entry_locale(local_string)) return error::INVALID_ENTRY; + + local = match(local_string) { + case let local = locale::locale => yield local; + case errors::invalid => return error::INVALID_ENTRY; + }; + }; + + return strings::dup(key), strings::dup(valu), local; +}; + +fn validate_entry_key(key: str) bool = { + return strings::contains(key, '[', ']', '='); +}; + +fn validate_entry_locale(key: str) bool = { + return strings::contains(key, '[', ']', '='); +};