Changes
4 changed files (+360/-3)
-
-
@@ -270,15 +270,57 @@ const step = b.step("run", "Build and run Plac GTK-Adwaita app");step.dependOn(&run.step); } // `zig build test` { // `zig build moo-test` const moo_test = moo_test: { var vala = try CompileVala.init(b, .{ .src = b.path("src/Moo"), .includeTestFiles = true, }); vala.addPackage("glib-2.0"); const test_exe = b.addExecutable(.{ .name = "moo_test", .root_module = b.createModule(.{ .link_libc = true, .target = target, .optimize = optimize, }), }); test_exe.linkSystemLibrary("glib-2.0"); test_exe.linkSystemLibrary("gobject-2.0"); for (vala.compiled_c_files) |file| { test_exe.root_module.addCSourceFile(.{ .file = file }); } const run = b.addRunArtifact(test_exe); const step = b.step("moo-test", "Test MOO message parsing"); step.dependOn(&run.step); break :moo_test run; }; // `zig build zig-test` const zig_test = zig_test: { const t = b.addTest(.{ .root_module = zig_lib.root_module, }); const run = b.addRunArtifact(t); const step = b.step("test", "Run unit tests"); const step = b.step("zig-test", "Test core module written in Zig"); step.dependOn(&run.step); break :zig_test run; }; // `zig build test` { const step = b.step("test", "Run unit tests"); step.dependOn(&zig_test.step); step.dependOn(&moo_test.step); } }
-
-
-
@@ -0,0 +1,198 @@// 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_metadata_tests() { Test.add_func("/libmoo/metadata/parse-realistic", () => { try { var metadata = new Metadata.from_string("MOO/1 REQUEST foo/bar\n"); assert(metadata.version == 1); assert(metadata.verb == "REQUEST"); assert(metadata.service == "foo/bar"); } catch (Error e) { assert_no_error(e); } try { var metadata = new Metadata.from_string("MOO/20 COMPLETE foo\n"); assert(metadata.version == 20); assert(metadata.verb == "COMPLETE"); assert(metadata.service == "foo"); } catch (Error e) { assert_no_error(e); } }); Test.add_func("/libmoo/metadata/reject-invalid-signatures", () => { string[] fixtures = { "", "\n", "MOO1 REQUEST foo/bar\n", "MEOW/1 REQUEST foo/bar\n" }; foreach (var fixture in fixtures) { try { new Metadata.from_string(fixture); message("Expected an error, but parsing succeeded"); assert_not_reached(); } catch (MetadataParseError e) { if (!(e is MetadataParseError.NOT_A_MOO_MESSAGE)) { message( "Expected an NOT_A_MOO_MESSAGE(%d), got %d", MetadataParseError.NOT_A_MOO_MESSAGE, e.code ); assert_not_reached(); } } } }); Test.add_func("/libmoo/metadata/reject-invalid-versions", () => { string[] fixtures = { "MOO/1.0 REQUEST foo/bar\n", "MOO/-1 REQUEST foo/bar\n", "MOO/ REQUEST foo/bar\n", "MOO/0x1 REQUEST foo/bar\n", "MOO/one REQUEST foo/bar\n", }; foreach (var fixture in fixtures) { try { new Metadata.from_string(fixture); message("Expected an error, but parsing succeeded"); message(fixture); assert_not_reached(); } catch (MetadataParseError e) { if (!(e is MetadataParseError.NOT_A_MOO_MESSAGE)) { message( "Expected an NOT_A_MOO_MESSAGE(%d), got %d", MetadataParseError.NOT_A_MOO_MESSAGE, e.code ); message(fixture); assert_not_reached(); } } } }); Test.add_func("/libmoo/metadata/reject-unsupported-versions", () => { string[] fixtures = { "MOO/0 REQUEST foo/bar\n" }; foreach (var fixture in fixtures) { try { new Metadata.from_string(fixture); message("Expected an error, but parsing succeeded"); message(fixture); assert_not_reached(); } catch (MetadataParseError e) { if (!(e is MetadataParseError.UNSUPPORTED_VERSION)) { message( "Expected an UNSUPPORTED_VERSION(%d), got %d", MetadataParseError.UNSUPPORTED_VERSION, e.code ); message(fixture); assert_not_reached(); } } } }); Test.add_func("/libmoo/metadata/reject-invalid-verbs", () => { string[] fixtures = { // lowercase "MOO/1 request foo/bar\n", // non-alphabet "MOO/1 RÉQUEST foo/bar\n", // number is not in alphabet "MOO/1 REQUEST1 foo/bar\n", // emoji "MOO/1 RE❓️UEST foo/bar\n", // missing "MOO/1 \n", // missing "MOO/1 foo/bar\n", }; foreach (var fixture in fixtures) { try { new Metadata.from_string(fixture); message("Expected an error, but parsing succeeded"); message(fixture); assert_not_reached(); } catch (MetadataParseError e) { if (!(e is MetadataParseError.NOT_A_MOO_MESSAGE)) { message( "Expected an NOT_A_MOO_MESSAGE(%d), got %d", MetadataParseError.NOT_A_MOO_MESSAGE, e.code ); message(fixture); assert_not_reached(); } } } }); Test.add_func("/libmoo/metadata/reject-invalid-services", () => { string[] fixtures = { "MOO/1 REQUEST \n", "MOO/1 REQUEST ", }; foreach (var fixture in fixtures) { try { new Metadata.from_string(fixture); message("Expected an error, but parsing succeeded"); message(fixture); assert_not_reached(); } catch (MetadataParseError e) { if (!(e is MetadataParseError.NOT_A_MOO_MESSAGE)) { message( "Expected an NOT_A_MOO_MESSAGE(%d), got %d", MetadataParseError.NOT_A_MOO_MESSAGE, e.code ); message(fixture); assert_not_reached(); } } } }); Test.add_func("/libmoo/metadata/reject-unsupported-services", () => { string[] fixtures = { // invalid UTF-8 bytes "MOO/1 REQUEST \xc3\x28\n", }; foreach (var fixture in fixtures) { try { new Metadata.from_string(fixture); message("Expected an error, but parsing succeeded"); message(fixture); assert_not_reached(); } catch (MetadataParseError e) { if (!(e is MetadataParseError.UNSUPPORTED_SERVICE_NAME)) { message( "Expected an UNSUPPORTED_SERVICE_NAME(%d), got %d", MetadataParseError.UNSUPPORTED_SERVICE_NAME, e.code ); message(fixture); assert_not_reached(); } } } }); } } }
-
-
src/Moo/Metadata.vala (new)
-
@@ -0,0 +1,96 @@// 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 MetadataParseError { NOT_A_MOO_MESSAGE, UNSUPPORTED_VERSION, UNSUPPORTED_SERVICE_NAME, } private const string SIGNATURE = "MOO/"; public class Metadata : Object { public uint32 version { get; construct; } public string verb { get; construct; } public string service { get; construct; } public int last_byte_index { get; construct; default = -1; } public Metadata(uint32 version = 1, string verb, string service) { Object(version: version, verb: verb, service: service); } public Metadata.from_string(string src) throws MetadataParseError { int i = 0; if (src.index_of(SIGNATURE) != 0) { throw new MetadataParseError.NOT_A_MOO_MESSAGE("Signature not found."); } i = SIGNATURE.length; // Parse version field var space_after_version = src.index_of_char(' ', i); if (space_after_version < 0 || space_after_version == i) { throw new MetadataParseError.NOT_A_MOO_MESSAGE("Missing version field."); } uint version; if (!uint.try_parse(src.slice((long) i, (long) space_after_version), out version, null, 10)) { throw new MetadataParseError.NOT_A_MOO_MESSAGE("Version is not a valid number."); } if (version == 0) { throw new MetadataParseError.UNSUPPORTED_VERSION("0 is not a valid version."); } if (version == 0 || version >= uint32.MAX) { throw new MetadataParseError.UNSUPPORTED_VERSION("Version does not fit in 32bit unsinged integer range."); } i = space_after_version + 1; // Parse verb (method) field var space_after_verb = src.index_of_char(' ', i); if (space_after_verb < 0 || space_after_verb == i) { throw new MetadataParseError.NOT_A_MOO_MESSAGE("Missing verb field."); } string verb = src.slice((long) i, (long) space_after_verb); foreach (var byte in verb.to_utf8()) { if (!byte.isupper()) { throw new MetadataParseError.NOT_A_MOO_MESSAGE("Verb contains non ASCII upper case letter."); } } i = space_after_verb + 1; // Parse service field var next_lf = src.index_of_char('\n', i); if (next_lf < 0 || next_lf == i) { throw new MetadataParseError.NOT_A_MOO_MESSAGE("Missing service field."); } string service = src.slice((long) i, (long) next_lf); if (!service.validate()) { throw new MetadataParseError.UNSUPPORTED_SERVICE_NAME("Service name is not a valid UTF8 text."); } i = next_lf + 1; Object(version: (uint32) version, verb: verb, service: service, last_byte_index: i); } } }
-
-
src/Moo/Moo.test.vala (new)
-
@@ -0,0 +1,21 @@// 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 void main(string[] args) { GLib.Test.init(ref args); Moo.Tests.add_metadata_tests(); GLib.Test.run(); }
-