Changes
4 changed files (+264/-0)
-
-
@@ -278,6 +278,7 @@ .includeTestFiles = true,}); vala.addPackage("glib-2.0"); vala.addPackage("gee-0.8"); const test_exe = b.addExecutable(.{ .name = "moo_test",
-
@@ -288,6 +289,7 @@ .optimize = optimize,}), }); test_exe.linkSystemLibrary("gee-0.8"); test_exe.linkSystemLibrary("glib-2.0"); test_exe.linkSystemLibrary("gobject-2.0");
-
-
-
@@ -0,0 +1,122 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; namespace Moo { namespace Tests { public void add_headers_tests() { Test.add_func("/libmoo/headers/parse-single", () => { try { var input = "MOO/1 REQUEST foo/bar\nFoo: Bar\n"; var metadata = new Metadata.from_string(input); var headers = new Headers.from_string(input, metadata); assert(headers.size == 1); assert(headers["foo"].size == 1); assert(headers["foo"][0] == "Bar"); assert(headers.last_byte_index == input.length); } catch (Error e) { assert_no_error(e); } }); Test.add_func("/libmoo/headers/parse-multi", () => { try { var input = """MOO/1 REQUEST foo/bar Foo: foo-value bar:bar-value baZ: baz-value """; var metadata = new Metadata.from_string(input); var headers = new Headers.from_string(input, metadata); assert(headers.size == 3); assert(headers["foo"][0] == "foo-value"); assert(headers["bar"][0] == "bar-value"); assert(headers["baz"][0] == "baz-value"); assert(headers.last_byte_index == input.length); } catch (Error e) { assert_no_error(e); } }); Test.add_func("/libmoo/headers/parse-without-last-lf", () => { try { var input = "MOO/1 REQUEST foo/bar\nFoo: Bar"; var metadata = new Metadata.from_string(input); var headers = new Headers.from_string(input, metadata); assert(headers.size == 1); assert(headers["foo"].size == 1); assert(headers["foo"][0] == "Bar"); assert(headers.last_byte_index == input.length); } catch (Error e) { assert_no_error(e); } }); Test.add_func("/libmoo/headers/empty-inputs", () => { string[] fixtures = { "MOO/1 REQUEST foo/bar\n", "MOO/1 REQUEST foo/bar\n\n", "MOO/1 REQUEST foo/bar\n\n\n" }; foreach (var fixture in fixtures) { try { var metadata = new Metadata.from_string(fixture); var headers = new Headers.from_string(fixture, metadata); assert(headers.size == 0); } catch (Error e) { assert_no_error(e); } } }); Test.add_func("/libmoo/headers/reject-empty-keys", () => { string[] fixtures = { "MOO/1 REQUEST foo/bar\n: Value", "MOO/1 REQUEST foo/bar\n : Value", }; foreach (var fixture in fixtures) { try { var metadata = new Metadata.from_string(fixture); new Headers.from_string(fixture, metadata); message("Expected an error, but parsing succeeded"); assert_not_reached(); } catch (HeadersParseError e) { if (!(e is HeadersParseError.EMPTY_KEY)) { message( "Expected an EMPTY_KEY(%d), got %d", HeadersParseError.EMPTY_KEY, e.code ); assert_not_reached(); } } catch (Error e) { assert_no_error(e); } } }); } } }
-
-
src/Moo/Headers.vala (new)
-
@@ -0,0 +1,139 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Moo { public errordomain HeadersParseError { NO_PARSING_CONTEXT, NO_DELIMITER, EMPTY_KEY, NON_ASCII_KEY, NON_UTF8_VALUE, } public class Headers : Object { private Gee.HashMap<string, Gee.ArrayList<string> >map; public int last_byte_index { get; construct; default = -1; } public int size { get { return map.size; } } public string? content_type { owned get { var entry = map["content-type"]; if (entry == null) { return null; } return entry[0]; } } public uint64 content_length { get { var entry = map["content-length"]; if (entry == null) { return 0; } var value = entry[0]; if (value == null) { return 0; } return uint64.parse(value); } } public uint64 request_id { get { var entry = map["request-id"]; if (entry == null) { return 0; } var value = entry[0]; if (value == null) { return 0; } return uint64.parse(value); } } /** * Parses `src`. * * Header keys will be ASCII lowercased. * * Throws an `NO_PARSING_CONTEXT` if `meta` is not created by * `Metadata.from_string()`. */ public Headers.from_string(string src, Metadata meta) throws HeadersParseError { if (meta.last_byte_index < 0) { throw new HeadersParseError.NO_PARSING_CONTEXT("Metadata does not contain parsed length."); } var map = new Gee.HashMap<string, Gee.ArrayList<string> >(); int i = meta.last_byte_index; while (i + 1 <= src.length) { var lf = src.index_of_char('\n', i); if (lf == i) { i += 1; break; } var line = src.slice(i, lf < 0 ? src.length : lf); i = lf < 0 ? i + line.length : lf + 1; var parts = line.split(":", 2); if (parts.length < 2) { throw new HeadersParseError.NO_DELIMITER("Header delimiter does not found."); } var key = parts[0].ascii_down().strip(); if (key.length == 0) { throw new HeadersParseError.EMPTY_KEY("Found an empty header key."); } if (!key.is_ascii()) { throw new HeadersParseError.NON_ASCII_KEY("Found a header key containing non-ASCII character."); } var value = parts[1].strip(); if (!value.validate()) { throw new HeadersParseError.NON_UTF8_VALUE("Found a header with non-UTF8 value."); } var existing = map[key]; if (existing != null) { existing.add(value); } else { var entry = new Gee.ArrayList<string>(); entry.add(value); map[key] = entry; } } Object(last_byte_index: i); this.map = map; } public new Gee.ArrayList<string>? @get(string key) { return map[key]; } } }
-
-
-
@@ -17,5 +17,6 @@void main(string[] args) { GLib.Test.init(ref args); Moo.Tests.add_metadata_tests(); Moo.Tests.add_headers_tests(); GLib.Test.run(); }
-