Move format::desktop_entry to format::xdg::ini
This commit is contained in:
8
format/xdg/ini/README
Normal file
8
format/xdg/ini/README
Normal file
@@ -0,0 +1,8 @@
|
||||
The ini module implements the basic INI-like format used by the XDG Desktop
|
||||
Entry Specification as described in
|
||||
(https://specifications.freedesktop.org/desktop-entry-spec/latest). Since other
|
||||
specifications make use of the basic desktop entry format (but with different
|
||||
group names, entry requirements, etc.), this module only implements the
|
||||
generalized format. This module will attempt to accept malformed files for the
|
||||
purposes of retaining their underlying representation, and being able to
|
||||
reproduce that representation when writing updated information to the files.
|
||||
60
format/xdg/ini/error.ha
Normal file
60
format/xdg/ini/error.ha
Normal file
@@ -0,0 +1,60 @@
|
||||
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 |
|
||||
invalid_boolean |
|
||||
invalid_numeric |
|
||||
invalid_integer |
|
||||
expected_single);
|
||||
|
||||
// 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;
|
||||
|
||||
// Returned when a boolean value was expected while parsing, but something else
|
||||
// was given.
|
||||
export type invalid_boolean = !void;
|
||||
|
||||
// Returned when a numeric value was expected while parsing, but something else
|
||||
// was given.
|
||||
export type invalid_numeric = !void;
|
||||
|
||||
// Returned when a numeric value was expected while parsing, but something else
|
||||
// was given.
|
||||
export type invalid_integer = !void;
|
||||
|
||||
// Returned when a singular value was expected while parsing, but multiple
|
||||
// values were given.
|
||||
export type expected_single = !void;
|
||||
|
||||
// Returns a user-friendly representation of [[error]]. The result may be
|
||||
// statically allocated.
|
||||
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 invalid_boolean => yield "invalid boolean";
|
||||
case invalid_numeric => yield "invalid numeric";
|
||||
case invalid_integer => yield "invalid integer";
|
||||
case expected_single => yield "expected single";
|
||||
case let err: io::error => yield io::strerror(err);
|
||||
case let err: utf8::invalid => yield utf8::strerror(err);
|
||||
};
|
||||
62
format/xdg/ini/line.ha
Normal file
62
format/xdg/ini/line.ha
Normal file
@@ -0,0 +1,62 @@
|
||||
use fmt;
|
||||
use io;
|
||||
use locale;
|
||||
use strings;
|
||||
|
||||
// A line in the file, which can be a comment (or a blank line), an entry, or
|
||||
// a 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 group header.
|
||||
// Specification: §3.2
|
||||
export type group_header = str;
|
||||
|
||||
// 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: str,
|
||||
locale: locale::locale,
|
||||
};
|
||||
|
||||
// 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),
|
||||
};
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// Formats a [[line]] and writes it to an [[io::handle]].
|
||||
export fn fprint(output: io::handle, lin: line) (size | io::error) = match (lin) {
|
||||
case blank => yield fmt::fprintln(output);
|
||||
case let lin: comment => yield fmt::fprintf(output, "#{}\n", lin: str);
|
||||
case let lin: group_header => yield fmt::fprintf(output, "[{}]\n", lin: str);
|
||||
case let lin: entry =>
|
||||
let wrote = fmt::print(lin.key)?;
|
||||
if (!locale::equal(locale::c, lin.locale)) {
|
||||
let localestr = locale::format(lin.locale);
|
||||
defer free(localestr);
|
||||
wrote += fmt::printf("[{}]", localestr)?;
|
||||
};
|
||||
wrote += fmt::printf("={}\n", lin.value)?;
|
||||
yield wrote;
|
||||
};
|
||||
133
format/xdg/ini/scan.ha
Normal file
133
format/xdg/ini/scan.ha
Normal file
@@ -0,0 +1,133 @@
|
||||
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 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;
|
||||
};
|
||||
};
|
||||
local.encoding = "";
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
146
format/xdg/ini/scan_test.ha
Normal file
146
format/xdg/ini/scan_test.ha
Normal file
@@ -0,0 +1,146 @@
|
||||
use fmt;
|
||||
use io;
|
||||
use locale;
|
||||
use os;
|
||||
|
||||
@test fn next() void = {
|
||||
let h_de = "Desktop Entry";
|
||||
let h_dag = "Desktop Action Gallery";
|
||||
let h_dac = "Desktop Action Create";
|
||||
let correct: []line = [
|
||||
"This is a comment": comment,
|
||||
blank,
|
||||
h_de: group_header,
|
||||
entry_new(h_de, "Version", "1.0", locale::c),
|
||||
entry_new(h_de, "Type", "Application", locale::c),
|
||||
entry_new(h_de, "Name", "Foo Viewer", locale::c),
|
||||
entry_new(h_de, "Comment", "The best viewer for Foo objects available!", locale::c),
|
||||
entry_new(h_de, "TryExec", "fooview", locale::c),
|
||||
entry_new(h_de, "Exec", "fooview %F", locale::c),
|
||||
blank,
|
||||
entry_new(h_de, "Icon", "fooview", locale::c),
|
||||
entry_new(h_de, "MimeType", "image/x-foo;", locale::c),
|
||||
entry_new(h_de, "Actions", "Gallery;Create;", locale::c),
|
||||
blank,
|
||||
h_dag: group_header,
|
||||
entry_new(h_dag, "Exec", "fooview --gallery", locale::c),
|
||||
entry_new(h_dag, "Name", "Browse Gallery", locale::c),
|
||||
blank,
|
||||
h_dac: group_header,
|
||||
entry_new(h_dac, "Exec", "fooview --create-new", locale::c),
|
||||
entry_new(h_dac, "Name", "Create a new Foo!", locale::c),
|
||||
entry_new(
|
||||
h_dac, "Name", "Create a new Foo!",
|
||||
locale::parse("en_US")!),
|
||||
entry_new(
|
||||
h_dac, "Name", "Zweep zoop flooble glorp",
|
||||
locale::parse("xx_XX.UTF-8")!),
|
||||
"Another comment": comment,
|
||||
entry_new(h_dac, "Icon", "fooview-new", locale::c),
|
||||
];
|
||||
|
||||
let file = os::open("format/desktop_entry/test_data/foo.desktop")!;
|
||||
defer io::close(file)!;
|
||||
let scanne = scan(file);
|
||||
defer finish(&scanne);
|
||||
|
||||
for (let correct .. correct) match(next(&scanne)!) {
|
||||
case io::EOF =>
|
||||
abort("encountered EOF early");
|
||||
case blank =>
|
||||
assert(correct is blank);
|
||||
case let ln: comment =>
|
||||
assert(ln == correct as comment);
|
||||
case let ln: group_header =>
|
||||
assert(ln == correct as group_header);
|
||||
case let ln: entry =>
|
||||
assert(entry_equal(ln, correct as entry));
|
||||
};
|
||||
|
||||
assert(next(&scanne) is io::EOF);
|
||||
};
|
||||
|
||||
@test fn next_entry() void = {
|
||||
let h_de = "Desktop Entry";
|
||||
let h_dag = "Desktop Action Gallery";
|
||||
let h_dac = "Desktop Action Create";
|
||||
let correct: []entry = [
|
||||
entry_new(h_de, "Version", "1.0", locale::c),
|
||||
entry_new(h_de, "Type", "Application", locale::c),
|
||||
entry_new(h_de, "Name", "Foo Viewer", locale::c),
|
||||
entry_new(h_de, "Comment", "The best viewer for Foo objects available!", locale::c),
|
||||
entry_new(h_de, "TryExec", "fooview", locale::c),
|
||||
entry_new(h_de, "Exec", "fooview %F", locale::c),
|
||||
entry_new(h_de, "Icon", "fooview", locale::c),
|
||||
entry_new(h_de, "MimeType", "image/x-foo;", locale::c),
|
||||
entry_new(h_de, "Actions", "Gallery;Create;", locale::c),
|
||||
entry_new(h_dag, "Exec", "fooview --gallery", locale::c),
|
||||
entry_new(h_dag, "Name", "Browse Gallery", locale::c),
|
||||
entry_new(h_dac, "Exec", "fooview --create-new", locale::c),
|
||||
entry_new(h_dac, "Name", "Create a new Foo!", locale::c),
|
||||
entry_new(
|
||||
h_dac, "Name", "Create a new Foo!",
|
||||
locale::parse("en_US")!),
|
||||
entry_new(
|
||||
h_dac, "Name", "Zweep zoop flooble glorp",
|
||||
locale::parse("xx_XX.UTF-8")!),
|
||||
entry_new(h_dac, "Icon", "fooview-new", locale::c),
|
||||
];
|
||||
|
||||
let file = os::open("format/desktop_entry/test_data/foo.desktop")!;
|
||||
defer io::close(file)!;
|
||||
let scanne = scan(file);
|
||||
defer finish(&scanne);
|
||||
|
||||
for (let correct .. correct) {
|
||||
assert(entry_equal(next_entry(&scanne)! as entry, correct));
|
||||
};
|
||||
|
||||
assert(next(&scanne) is io::EOF);
|
||||
};
|
||||
|
||||
@test fn parse_entry() void = {
|
||||
assert(entry_equal(parse_entry("hello=world")!, entry {
|
||||
key = "hello",
|
||||
value = "world",
|
||||
locale = locale::c,
|
||||
...
|
||||
}));
|
||||
assert(entry_equal(parse_entry("hello[sr_YU.UTF-8@Latn]=world")!, entry {
|
||||
key = "hello",
|
||||
value = "world",
|
||||
locale = locale::parse("sr_YU.UTF-8@Latn")!,
|
||||
...
|
||||
}));
|
||||
};
|
||||
|
||||
@test fn validate_entry_key() void = {
|
||||
// §3.3 Only the characters A-Za-z0-9- may be used in key names.
|
||||
assert(validate_entry_key("SomeName") is void);
|
||||
assert(validate_entry_key("some-name") is void);
|
||||
assert(validate_entry_key("1234567890some-0987654321naMe") is void);
|
||||
assert(validate_entry_key("^&(*)[") is invalid_entry);
|
||||
assert(validate_entry_key("]") is invalid_entry);
|
||||
assert(validate_entry_key("&*%^") is invalid_entry);
|
||||
assert(validate_entry_key("asj=hd@#*()") is invalid_entry);
|
||||
};
|
||||
|
||||
@test fn validate_entry_locale() void = {
|
||||
assert(validate_entry_locale("sr_YU.UTF-8@Latn") is void);
|
||||
assert(validate_entry_locale("abcd[") is invalid_entry);
|
||||
assert(validate_entry_locale("ab]cd") is invalid_entry);
|
||||
assert(validate_entry_locale("a=bcd") is invalid_entry);
|
||||
};
|
||||
|
||||
fn entry_equal(a: entry, b: entry) bool =
|
||||
a.group == b.group &&
|
||||
a.key == b.key &&
|
||||
a.value == b.value &&
|
||||
locale::equal(a.locale, b.locale);
|
||||
|
||||
fn entry_new(group: str, key: str, value: str, local: locale::locale) entry = entry {
|
||||
group = group,
|
||||
key = key,
|
||||
value = value,
|
||||
locale = local,
|
||||
};
|
||||
25
format/xdg/ini/test_data/foo.desktop
Normal file
25
format/xdg/ini/test_data/foo.desktop
Normal file
@@ -0,0 +1,25 @@
|
||||
#This is a comment
|
||||
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Foo Viewer
|
||||
Comment=The best viewer for Foo objects available!
|
||||
TryExec=fooview
|
||||
Exec=fooview %F
|
||||
|
||||
Icon=fooview
|
||||
MimeType=image/x-foo;
|
||||
Actions=Gallery;Create;
|
||||
|
||||
[Desktop Action Gallery]
|
||||
Exec=fooview --gallery
|
||||
Name=Browse Gallery
|
||||
|
||||
[Desktop Action Create]
|
||||
Exec=fooview --create-new
|
||||
Name=Create a new Foo!
|
||||
Name[en_US]=Create a new Foo!
|
||||
Name[xx_XX.UTF-8]=Zweep zoop flooble glorp
|
||||
#Another comment
|
||||
Icon=fooview-new
|
||||
28
format/xdg/ini/test_data/foo_full.desktop
Normal file
28
format/xdg/ini/test_data/foo_full.desktop
Normal file
@@ -0,0 +1,28 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Foo Viewer
|
||||
GenericName=Foo Viewer
|
||||
GenericName[xx_XX.UTF-8]=Sneep glorp
|
||||
Comment=The best viewer for Foo objects available!
|
||||
TryExec=fooview
|
||||
Exec=fooview %F
|
||||
Icon=fooview
|
||||
MimeType=image/x-foo;
|
||||
Actions=Gallery;Create;
|
||||
OnlyShowIn=MATE;KDE;
|
||||
NotShowIn=GNOME
|
||||
Categories=Graphics;Utility
|
||||
Implements=com.example.Example;com.example.OtherExample
|
||||
Keywords=foo;image;view;viewer
|
||||
|
||||
[Desktop Action Gallery]
|
||||
Exec=fooview --gallery
|
||||
Name=Browse Gallery
|
||||
|
||||
[Desktop Action Create]
|
||||
Exec=fooview --create-new
|
||||
Name=Create a new Foo!
|
||||
Name[en_US]=Create a new Foo!
|
||||
Name[xx_XX.UTF-8]=Zweep zoop flooble glorp
|
||||
Icon=fooview-new
|
||||
207
format/xdg/ini/value.ha
Normal file
207
format/xdg/ini/value.ha
Normal file
@@ -0,0 +1,207 @@
|
||||
use ascii;
|
||||
use strconv;
|
||||
use strings;
|
||||
|
||||
// Parses a string value. It may contain all ASCII characters except for control
|
||||
// characters. The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_string(in: str) (str | error) = {
|
||||
for (let char .. strings::toutf8(in)) {
|
||||
if (!ascii::valid(char: rune)) return invalid_ascii;
|
||||
};
|
||||
return parse_localestring(in);
|
||||
};
|
||||
|
||||
// Parses a localestring value. It is user displayable, and is encoded in UTF-8.
|
||||
// The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_localestring(in: str) (str | error) = {
|
||||
let escaper = escape_string(in);
|
||||
let string = match(string_escaper_next(&escaper)?) {
|
||||
case let string: str => yield string;
|
||||
case done => return "";
|
||||
};
|
||||
if (len(escaper) > 0) {
|
||||
free(string);
|
||||
return expected_single;
|
||||
};
|
||||
return string;
|
||||
};
|
||||
|
||||
// Parses an iconstring value. It is the name of an icon; it may be an absolute
|
||||
// path, or a symbolic name for an icon located using the algorithm described in
|
||||
// the Icon Theme Specification. Such values are not user-displayable, and are
|
||||
// encoded in UTF-8. The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_iconstring(in: str) (str | error) = parse_localestring(in);
|
||||
|
||||
// Parses a boolean value. It must either be the string "true" or "false".
|
||||
// Specification: §4
|
||||
export fn parse_boolean(in: str) (bool | error) = {
|
||||
if (strings::contains(in, ";")) return expected_single;
|
||||
if (in == "true" ) return true;
|
||||
if (in == "false") return false;
|
||||
return invalid_boolean;
|
||||
};
|
||||
|
||||
// Parses a numeric value. It must be a valid floating point number as
|
||||
// recognized by the %f specifier for scanf in the C locale.
|
||||
// Specification: §4
|
||||
export fn parse_numeric(in: str) (f32 | error) = match (strconv::stof32(in)) {
|
||||
case let float: f32 => yield float;
|
||||
case =>
|
||||
if (strings::contains(in, ";")) return expected_single;
|
||||
yield invalid_numeric;
|
||||
};
|
||||
|
||||
// Parses an integer value. While there is no documentation on this data type,
|
||||
// it is referred to by the Icon Theme Specification, §4, tables 2-3.
|
||||
export fn parse_integer(in: str) (int | error) = match(strconv::stoi(in)) {
|
||||
case let integer: int => yield integer;
|
||||
case =>
|
||||
if (strings::contains(in, ";")) return expected_single;
|
||||
yield invalid_integer;
|
||||
};
|
||||
|
||||
// Parses multiple string values. See [[parse_string]] for more information. The
|
||||
// memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_strings(in: str) ([]str | error) = {
|
||||
for (let char .. strings::toutf8(in)) {
|
||||
if (!ascii::valid(char: rune)) return invalid_ascii;
|
||||
};
|
||||
return parse_localestrings(in);
|
||||
};
|
||||
|
||||
// Parses multiple localestring values. See [[parse_localestring]] for more
|
||||
// information. The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_localestrings(in: str) ([]str | error) = {
|
||||
let escaper = escape_string(in);
|
||||
let result: []str = alloc([], 0);
|
||||
for (let in => string_escaper_next(&escaper)) {
|
||||
match (in) {
|
||||
case let string: str =>
|
||||
append(result, string);
|
||||
case let err: error =>
|
||||
strings::freeall(result);
|
||||
return err;
|
||||
};
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
// Parses multiple iconstring values. See [[parse_iconstring]] for more
|
||||
// information. The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_iconstrings(in: str) ([]str | error) = parse_localestrings(in);
|
||||
|
||||
// Parses multiple boolean values. See [[parse_boolean]] for more information.
|
||||
// The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_booleans(in: str) ([]bool | error) = {
|
||||
let splitte = split(in);
|
||||
let result: []bool = alloc([], 0);
|
||||
for (let in => splitter_next(&splitte)) {
|
||||
match (parse_boolean(in)) {
|
||||
case let boolean: bool =>
|
||||
append(result, boolean);
|
||||
case let err: error =>
|
||||
free(result);
|
||||
return err;
|
||||
};
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
// Parses multiple numeric values. See [[parse_numeric]] for more information.
|
||||
// The memory must be freed by the caller.
|
||||
// Specification: §4
|
||||
export fn parse_numerics(in: str) ([]f32 | error) = {
|
||||
let splitte = split(in);
|
||||
let result: []f32 = alloc([], 0);
|
||||
for (let in => splitter_next(&splitte)) {
|
||||
match (parse_numeric(in)) {
|
||||
case let number: f32 =>
|
||||
append(result, number);
|
||||
case let err: error =>
|
||||
free(result);
|
||||
return err;
|
||||
};
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
// Parses multiple integer values. See [[parse_integer]] for more information.
|
||||
// The memory must be freed by the caller.
|
||||
export fn parse_integers(in: str) ([]int | error) = {
|
||||
let splitte = split(in);
|
||||
let result: []int = alloc([], 0);
|
||||
for (let in => splitter_next(&splitte)) {
|
||||
match (parse_integer(in)) {
|
||||
case let number: int =>
|
||||
append(result, number);
|
||||
case let err: error =>
|
||||
free(result);
|
||||
return err;
|
||||
};
|
||||
};
|
||||
return result;
|
||||
};
|
||||
|
||||
type string_escaper = []u8;
|
||||
|
||||
fn escape_string(in: str) string_escaper = strings::toutf8(in): string_escaper;
|
||||
|
||||
fn string_escaper_next(this: *string_escaper) (str | done | error) = {
|
||||
if (len(*this) < 1) return done;
|
||||
let result: []u8 = alloc([], 0);
|
||||
let end = 0z;
|
||||
let saw_escape = false;
|
||||
for (let byte .. *this) {
|
||||
end += 1;
|
||||
if (saw_escape) {
|
||||
switch (byte) {
|
||||
case ';' => append(result, ';');
|
||||
case 's' => append(result, ' ');
|
||||
case 'n' => append(result, '\n');
|
||||
case 't' => append(result, '\t');
|
||||
case 'r' => append(result, '\r');
|
||||
case '\\' => append(result, '\\');
|
||||
case =>
|
||||
free(result);
|
||||
return invalid_escape;
|
||||
};
|
||||
saw_escape = false;
|
||||
} else {
|
||||
switch (byte) {
|
||||
case ';' => break;
|
||||
case '\\' => saw_escape = true;
|
||||
case => append(result, byte);
|
||||
};
|
||||
};
|
||||
};
|
||||
*this = (*this)[end..];
|
||||
return strings::fromutf8_unsafe(result);
|
||||
};
|
||||
|
||||
type splitter = []u8;
|
||||
|
||||
fn split(in: str) splitter = strings::toutf8(in): splitter;
|
||||
|
||||
fn splitter_next(this: *splitter) (str | done) = {
|
||||
if (len(*this) < 1) return done;
|
||||
let end = 0z;
|
||||
let knife = 0z;
|
||||
for (let byte .. *this) {
|
||||
knife += 1;
|
||||
if (byte == ';') {
|
||||
break;
|
||||
} else {
|
||||
end += 1;
|
||||
};
|
||||
};
|
||||
let result = this[..end];
|
||||
*this = (*this)[knife..];
|
||||
return strings::fromutf8_unsafe(result);
|
||||
};
|
||||
232
format/xdg/ini/value_test.ha
Normal file
232
format/xdg/ini/value_test.ha
Normal file
@@ -0,0 +1,232 @@
|
||||
use fmt;
|
||||
use math;
|
||||
use strings;
|
||||
|
||||
@test fn parse_string() void = {
|
||||
assert(parse_string("hello")! == "hello");
|
||||
assert(parse_string("hel\\s\\n\\t\\r\\\\\\;lo")! == "hel \n\t\r\\;lo");
|
||||
assert(parse_string("hello☠") is invalid_ascii);
|
||||
assert(parse_string("hello;world") is expected_single);
|
||||
assert(parse_string("hello\\d") is invalid_escape);
|
||||
};
|
||||
|
||||
@test fn parse_localestring() void = {
|
||||
assert(parse_localestring("hello")! == "hello");
|
||||
assert(parse_localestring("hel\\s\\n\\t\\r\\\\\\;lo")! == "hel \n\t\r\\;lo");
|
||||
assert(parse_localestring("hello☠")! == "hello☠");
|
||||
assert(parse_localestring("hello;world") is expected_single);
|
||||
assert(parse_localestring("hello\\d") is invalid_escape);
|
||||
};
|
||||
|
||||
@test fn parse_iconstring() void = {
|
||||
assert(parse_iconstring("hello")! == "hello");
|
||||
assert(parse_iconstring("hel\\s\\n\\t\\r\\\\\\;lo")! == "hel \n\t\r\\;lo");
|
||||
assert(parse_iconstring("hello☠")! == "hello☠");
|
||||
assert(parse_iconstring("hello;world") is expected_single);
|
||||
assert(parse_iconstring("hello\\d") is invalid_escape);
|
||||
};
|
||||
|
||||
@test fn parse_boolean() void = {
|
||||
assert(parse_boolean("true")! == true);
|
||||
assert(parse_boolean("false")! == false);
|
||||
assert(parse_boolean("hello") is invalid_boolean);
|
||||
assert(parse_boolean("ttrue") is invalid_boolean);
|
||||
assert(parse_boolean("falsee") is invalid_boolean);
|
||||
assert(parse_boolean("") is invalid_boolean);
|
||||
assert(parse_boolean("1") is invalid_boolean);
|
||||
assert(parse_boolean("0") is invalid_boolean);
|
||||
assert(parse_boolean("true;false") is expected_single);
|
||||
};
|
||||
|
||||
@test fn parse_numeric() void = {
|
||||
assert(parse_numeric("9")! == 9.0f32);
|
||||
assert(parse_numeric("9.0")! == 9.0f32);
|
||||
assert(parse_numeric("34.93")! == 34.93f32);
|
||||
assert(parse_numeric("-100.895")! == -100.895f32);
|
||||
assert(math::isnan(parse_numeric("NaN")!));
|
||||
assert(parse_numeric("Infinity")! == math::INF);
|
||||
assert(parse_numeric("hello") is invalid_numeric);
|
||||
assert(parse_numeric("--") is invalid_numeric);
|
||||
assert(parse_numeric("....") is invalid_numeric);
|
||||
assert(parse_numeric("234;7.4") is expected_single);
|
||||
};
|
||||
|
||||
@test fn parse_integer() void = {
|
||||
assert(parse_integer("9")! == 9);
|
||||
assert(parse_integer("2348")! == 2348);
|
||||
assert(parse_integer("-324")! == -324);
|
||||
assert(parse_integer("324.9") is invalid_integer);
|
||||
assert(parse_integer("324.0") is invalid_integer);
|
||||
};
|
||||
|
||||
@test fn parse_strings() void = {
|
||||
let correct: []str = [
|
||||
"b\r\tird",
|
||||
"wa;ter",
|
||||
"",
|
||||
"riv\ner",
|
||||
"nuh uh",
|
||||
];
|
||||
let got = parse_strings("b\\r\\tird;wa\\;ter;;riv\\ner;nuh uh")!;
|
||||
defer strings::freeall(got);
|
||||
for (let index = 0z; index < len(correct); index += 1) {
|
||||
assert(index < len(got), "ran out");
|
||||
assert(compare_strings(correct[index], got[index]));
|
||||
};
|
||||
assert(len(got) == len(correct), "not done");
|
||||
assert(parse_strings("hello☠;world") is invalid_ascii);
|
||||
assert(parse_strings("hello\\d;world") is invalid_escape);
|
||||
};
|
||||
|
||||
@test fn parse_localestrings() void = {
|
||||
let correct: []str = [
|
||||
"b\r\tir☠d",
|
||||
"wa;ter",
|
||||
"",
|
||||
"ri☠v\ner",
|
||||
"nuh uh",
|
||||
];
|
||||
let got = parse_localestrings("b\\r\\tir☠d;wa\\;ter;;ri☠v\\ner;nuh uh")!;
|
||||
defer strings::freeall(got);
|
||||
for (let index = 0z; index < len(correct); index += 1) {
|
||||
assert(index < len(got), "ran out");
|
||||
assert(compare_strings(correct[index], got[index]));
|
||||
};
|
||||
assert(len(got) == len(correct), "not done");
|
||||
assert(parse_strings("hello\\d;world") is invalid_escape);
|
||||
};
|
||||
|
||||
@test fn parse_booleans() void = {
|
||||
let correct: []bool = [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
];
|
||||
let got = parse_booleans("true;true;false;true;false;true")!;
|
||||
defer free(got);
|
||||
for (let index = 0z; index < len(correct); index += 1) {
|
||||
assert(index < len(got), "ran out");
|
||||
let correct = correct[index];
|
||||
let got = got[index];
|
||||
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||
assert(correct == got);
|
||||
};
|
||||
assert(len(got) == len(correct), "not done");
|
||||
assert(parse_booleans("hello;world") is invalid_boolean);
|
||||
assert(parse_booleans("true;;") is invalid_boolean);
|
||||
assert(parse_booleans(";false;") is invalid_boolean);
|
||||
};
|
||||
|
||||
@test fn parse_numerics() void = {
|
||||
let correct: []f64 = [
|
||||
5.0f32,
|
||||
34.9f32,
|
||||
29.0f32,
|
||||
32498.23784f32,
|
||||
];
|
||||
let got = parse_numerics("5;34.9;29;32498.23784;")!;
|
||||
defer free(got);
|
||||
for (let index = 0z; index < len(correct); index += 1) {
|
||||
assert(index < len(got), "ran out");
|
||||
let correct = correct[index];
|
||||
let got = got[index];
|
||||
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||
assert(correct == got);
|
||||
};
|
||||
assert(len(got) == len(correct), "not done");
|
||||
assert(parse_numerics("hello;world") is invalid_numeric);
|
||||
assert(parse_numerics("5;;") is invalid_numeric);
|
||||
assert(parse_numerics(";5;") is invalid_numeric);
|
||||
};
|
||||
|
||||
@test fn parse_integers() void = {
|
||||
let correct: []int = [
|
||||
5,
|
||||
34,
|
||||
-29,
|
||||
32498,
|
||||
];
|
||||
let got = parse_integers("5;34;-29;32498")!;
|
||||
defer free(got);
|
||||
for (let index = 0z; index < len(correct); index += 1) {
|
||||
assert(index < len(got), "ran out");
|
||||
let correct = correct[index];
|
||||
let got = got[index];
|
||||
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||
assert(correct == got);
|
||||
};
|
||||
assert(len(got) == len(correct), "not done");
|
||||
assert(parse_integers("hello;world") is invalid_integer);
|
||||
assert(parse_integers("5;;") is invalid_integer);
|
||||
assert(parse_integers(";5;") is invalid_integer);
|
||||
};
|
||||
|
||||
@test fn string_escaper_next_a() void = {
|
||||
let escaper = escape_string("bird;water;;river;");
|
||||
let correct: []str = [
|
||||
"bird",
|
||||
"water",
|
||||
"",
|
||||
"river",
|
||||
];
|
||||
for (let correct .. correct) {
|
||||
let got = string_escaper_next(&escaper) as str;
|
||||
assert(compare_strings(correct, got));
|
||||
};
|
||||
assert(string_escaper_next(&escaper) is done, "not done");
|
||||
};
|
||||
|
||||
@test fn string_escaper_next_b() void = {
|
||||
let escaper = escape_string("b\\r\\tird;wa\\;ter;;riv\\ner;nuh uh");
|
||||
let correct: []str = [
|
||||
"b\r\tird",
|
||||
"wa;ter",
|
||||
"",
|
||||
"riv\ner",
|
||||
"nuh uh",
|
||||
];
|
||||
for (let correct .. correct) {
|
||||
let got = string_escaper_next(&escaper) as str;
|
||||
assert(compare_strings(correct, got));
|
||||
};
|
||||
assert(string_escaper_next(&escaper) is done, "not done");
|
||||
};
|
||||
|
||||
@test fn splitter_next_a() void = {
|
||||
let splitte = split("bird;water;;river;");
|
||||
let correct: []str = [
|
||||
"bird",
|
||||
"water",
|
||||
"",
|
||||
"river",
|
||||
];
|
||||
for (let correct .. correct) {
|
||||
let got = splitter_next(&splitte) as str;
|
||||
assert(compare_strings(correct, got));
|
||||
};
|
||||
assert(splitter_next(&splitte) is done, "not done");
|
||||
};
|
||||
|
||||
@test fn splitter_next_b() void = {
|
||||
let splitte = split("bird;water;;river");
|
||||
let correct: []str = [
|
||||
"bird",
|
||||
"water",
|
||||
"",
|
||||
"river",
|
||||
];
|
||||
for (let correct .. correct) {
|
||||
let got = splitter_next(&splitte) as str;
|
||||
fmt::printf("[{}]\t[{}]\n", correct, got)!;
|
||||
assert(compare_strings(correct, got));
|
||||
};
|
||||
assert(splitter_next(&splitte) is done, "not done");
|
||||
};
|
||||
|
||||
fn compare_strings(a: str, b: str) bool = {
|
||||
fmt::printf("[{}]\t[{}]\n", a, b)!;
|
||||
return a == b;
|
||||
};
|
||||
Reference in New Issue
Block a user