Compare commits

...

10 Commits

8 changed files with 204 additions and 197 deletions

View File

@ -1,198 +1,44 @@
use locale;
use strings;
// Represents a file of the generic key/value format used by desktop entries.
// Specification: §3
export type file = struct {
preceeding_lines: []line,
groups: []group,
};
// A named group of key/value entries.
// Specification: §3.2
export type group = struct {
name: str,
lines: []line,
};
// 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 | group_header | entry);
// A blank line.
// Specification: §3.1
export type blank = void;
// A comment.
// Specification: §3.1
export type comment = str;
// A key/value pair.
// Specification: §3.3
export type entry = struct {
key: str,
value: value,
};
// A group header.
// Specification: §3.2
export type group_header = str;
// A localized entry.
// Specification: §5
export type localized_entry = struct {
// An entry in a desktop file. Entries without an explicitly stated locale are
// assigned [[locale::c]].
// Specification: §3.3, §5
export type entry = struct {
group: str,
key: str,
value: value,
value: str,
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.
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];
case void => return void;
};
let lin = match (group_find_entry(grou, key)) {
case let index: size => yield grou.lines[index];
case void => return void;
};
match (lin) {
case let entr: entry => return entr.value;
case => abort();
};
// Duplicates an [[entry]]. Use [[entry_finish]] to get rid of it.
export fn entry_dup(entr: entry) entry = entry {
group = strings::dup(entr.group),
key = strings::dup(entr.key),
value = strings::dup(entr.value),
locale = locale::dup(entr.locale),
};
// Gets a localized value from a file. If the group does not exist, or the group
// exists but the key isnt in it, it returns void. If the key is in the group
// but is not localized to the specified locale, the non-localized value is
// returned.
export fn file_get_localized(
fil: *file,
group_name: str,
key: str,
local: locale::locale,
) (value | void) = {
let grou = match (file_find_group(fil, group_name)) {
case let index: size => yield &fil.groups[index];
case void => return void;
};
let lin = match (group_find_localized_entry(grou, key, local)) {
case let index: size => yield grou.lines[index];
case void =>
yield match(group_find_entry(grou, key)) {
case let index: size => yield index;
case void => return void;
};
};
match (lin) {
case let entr: localized_entry => return entr.value;
case let entr: entry => return entr.value;
case => abort();
};
};
//export fn file_add
//export fn file_add_localized
//export fn file_remove
//export fn file_remove_localized
//export fn file_encode
//export fn file_finish
fn file_find_group(fil: *file, group_name: str) (size | void) = {
let index = 0z;
for (let grou .. fil.groups) {
if (grou.name == group_name) {
return index;
};
index += 1;
};
};
fn group_find_entry(grou: *group, key: str) (size | void) = {
let index = 0z;
for (let lin .. grou.lines) {
match (lin) {
case let entr: entry =>
if (entr.key == key) {
return index;
};
case => void;
};
index += 1;
};
};
fn group_find_localized_entry(grou: *group, key: str, local: locale::locale) (size | void) = {
// The matching is done as follows. If LC_MESSAGES is of the form
// lang_COUNTRY.ENCODING@MODIFIER, then it will match a key of the form
// lang_COUNTRY@MODIFIER. If such a key does not exist, it will attempt
// to match lang_COUNTRY followed by lang@MODIFIER. Then, a match
// against lang by itself will be attempted. Finally, if no matching
// key is found the required key without a locale specified is used.
// The encoding from the LC_MESSAGES value is ignored when matching.
//
// If LC_MESSAGES does not have a MODIFIER field, then no key with a
// modifier will be matched. Similarly, if LC_MESSAGES does not have a
// COUNTRY field, then no key with a country specified will be matched.
// If LC_MESSAGES just has a lang field, then it will do a straight
// match to a key with a similar value.
let lang_country_modifier = local;
lang_country_modifier.encoding = "";
match(group_find_localized_entry_exact(grou, key, lang_country_modifier)) {
case let index: size => return index;
case => void;
};
let lang_country = lang_country_modifier;
lang_country.modifier = "";
match(group_find_localized_entry_exact(grou, key, lang_country)) {
case let index: size => return index;
case => void;
};
let lang_modifier = lang_country_modifier;
lang_modifier.country = "";
match(group_find_localized_entry_exact(grou, key, lang_modifier)) {
case let index: size => return index;
case => void;
};
let lang = lang_modifier;
lang.modifier = "";
match(group_find_localized_entry_exact(grou, key, lang)) {
case let index: size => return index;
case => void;
};
return 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) {
case let entr: localized_entry =>
if (entr.key == key && locale::equal(entr.locale, local)) {
return index;
};
case => void;
};
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;
// Frees memory associated with an [[entry]].
export fn entry_finish(entr: entry) void = {
free(entr.group);
free(entr.key);
free(entr.value);
locale::finish(entr.locale);
};

View File

@ -0,0 +1,35 @@
use encoding::utf8;
use io;
// Any error that may occur during parsing.
export type error = !(syntaxerr | io::error | utf8::invalid);
// All syntax errors defined in this module.
export type syntaxerr = !(
invalid_group_header |
invalid_entry |
invalid_ascii |
invalid_escape);
// Returned when a malformed group header is encountered while parsing.
export type invalid_group_header = !void;
// Returned when a malformed entry is encountered while parsing.
export type invalid_entry = !void;
// Returned when ASCII text was expected while parsing, but something else was
// given.
export type invalid_ascii = !void;
// Returned when an invalid escape sequence was enctountered while parsing.
export type invalid_escape = !void;
// Converts a desktop entry [[error]] into a user-friendly string.
export fn strerror(err: error) str = match (err) {
case invalid_group_header => yield "invalid group header";
case invalid_entry => yield "invalid entry";
case invalid_ascii => yield "invalid ascii";
case invalid_escape => yield "invalid escape";
case let err: io::error => yield io::strerror(err);
case let err: utf8::invalid => yield utf8::strerror(err);
};

View File

@ -0,0 +1,122 @@
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 [[line_dup]] to retain a copy. If all you
// want is the file's data, use next_entry.
// FIXME: there is no line_dup
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 = 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));
if (!validate_entry_key(key)) return invalid_entry;
let (key, local_string) = strings::cut(key, "[");
let local = if (local_string != "") {
yield locale::c;
} else {
local_string = strings::rtrim(local_string, ']');
if (!validate_entry_locale(local_string)) return invalid_entry;
yield match(locale::parse(local_string)) {
case let local: locale::locale => yield local;
case locale::invalid => return invalid_entry;
};
};
return entry {
key = key,
value = valu,
locale = local,
...
};
};
fn validate_entry_key(key: str) bool = {
return strings::contains(key, '[', ']', '=');
};
fn validate_entry_locale(key: str) bool = {
return strings::contains(key, '[', ']', '=');
};

View File

@ -52,29 +52,29 @@ fn get_locale(var: str) locale =
};
};
fn get_locale_no_fallback(var: str) (locale | errors::invalid) =
fn get_locale_no_fallback(var: str) (locale | invalid) =
match (get_env_locale(var)) {
case let local: locale => yield local;
case => yield (get_locale_conf_entry(var));
};
fn get_env_locale(var: str) (locale | errors::invalid) =
fn get_env_locale(var: str) (locale | invalid) =
match (os::getenv(var)) {
case let env: str => return parse(env);
case => return errors::invalid;
case => return invalid;
};
let locale_conf: []str = [];
@fini fn locale_conf() void = strings::freeall(locale_conf);
fn get_locale_conf_entry(var: str) (locale | errors::invalid) = {
fn get_locale_conf_entry(var: str) (locale | invalid) = {
get_locale_conf();
for (let entry .. locale_conf) {
let (key, value) = strings::cut(entry, "=");
if (key == var) return parse(value);
};
return errors::invalid;
return invalid;
};
fn get_locale_conf() []str = {

View File

@ -1,5 +1,3 @@
use errors;
// Returns the locale to use for character classification and case conversion.
// The memory is statically allocated and must not be free'd. It may be
// overwritten later, so use [[dup]] to extend its lifetime.

7
locale/error.ha Normal file
View File

@ -0,0 +1,7 @@
// All errors defined in this module.
export type error = !invalid;
// Returned when a malformed locale is encountered.
export type invalid = !void;
export fn strerror(err: error) str = "invalid locale";

View File

@ -21,16 +21,16 @@ export def c = locale {
// lang_COUNTRY.ENCODING@MODIFIER
//
// Where _COUNTRY, .ENCODING, and @MODIFIER may be omitted. The function
// returns a [[locale]], or [[errors::invalid]] if the input cannot be parsed.
// All memory is borrowed from the input, so [[finish]] should not be used to
// free it.
export fn parse(in: str) (locale | errors::invalid) = {
// returns a [[locale]], or [[invalid]] if the input cannot be parsed. All
// memory is borrowed from the input, so [[finish]] should not be used to free
// it.
export fn parse(in: str) (locale | invalid) = {
let (in, modifier) = strings::rcut(in, "@");
if (strings::compare(in, "") == 0) return void: errors::invalid;
if (strings::compare(in, "") == 0) return void: invalid;
let (in, encoding) = strings::rcut(in, ".");
if (strings::compare(in, "") == 0) return void: errors::invalid;
if (strings::compare(in, "") == 0) return void: invalid;
let (in, country) = strings::rcut(in, "_");
if (strings::compare(in, "") == 0) return void: errors::invalid;
if (strings::compare(in, "") == 0) return void: invalid;
return locale {
lang = in,
country = country,

View File

@ -1,5 +1,4 @@
use fmt;
use errors;
use strings;
@test fn parse_full() void = {
@ -64,8 +63,8 @@ use strings;
@test fn parse_error() void = {
let local = match(parse("_COUNTRY.ENCODING@MODIFIER")) {
case errors::invalid => void;
case => abort("error");
case invalid => void;
case => abort("no error");
};
};