-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
// Copyright 2025 Shota FUJI
//
// Licensed under the Zero-Clause BSD License or the Apache License, Version 2.0, at your option.
// You may not use, copy, modify, or distribute this file except according to those terms. You can
// find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt, and a copy of the Apache License,
// Version 2.0 at LICENSES/Apache-2.0.txt. You may also obtain a copy of the Zero-Clause BSD License
// at <https://opensource.org/license/0bsd> and a copy of the Apache License, Version 2.0 at
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// SPDX-License-Identifier: 0BSD OR Apache-2.0
//
// ===
//
// Zig API.
//! MOO message is a message format used for communicating with Roon Server.
//! Encodes to and decodes from newline-delimited UTF-8 encoded plain text, like HTTP.
//! Newline character is LF (0xa) without CR character.
//!
//! MOO message consists of metadata, header and body: header is a list of key-values
//! that both key and value contains UTF-8 string value, and body is single UTF-8 string.
//! Typical MOO message have "Content-Type" header that describes how the message's body
//! is encoded. (e.g. "application/json")
//! The first line of MOO message is metadata line, which is in "MOO/VERSION VERB SERVICE"
//! format. SERVICE is delimited by slash.
const std = @import("std");
const HeaderParsingContext = struct {
start_position: usize,
};
/// Metadata contains a MOO message's version and semantics.
/// Fields point to the source message bytes: freeing the source message bytes then
/// accessing fields results in use-after-free.
pub const Metadata = struct {
/// Message schema version described in metadata line.
version: u32 = 1,
/// Verb described in metadata line. Verb consists of uppercase alphabets.
/// @example: "REQUEST", "COMPLETE"
verb: []const u8,
/// Service name described in metadata line.
service: []const u8,
context: HeaderParsingContext,
const signature = "MOO/";
pub const ParseError = error{
InvalidSignature,
InvalidVersionNumber,
InvalidVerb,
InvalidService,
NonUtf8ServiceName,
};
pub fn parse(message: []const u8) ParseError!@This() {
var i: usize = 0;
if (!std.mem.startsWith(u8, message, signature)) {
return ParseError.InvalidSignature;
}
i += signature.len;
const version = if (std.mem.indexOfScalarPos(u8, message, i, ' ')) |space_position| blk: {
if (i == space_position) {
return ParseError.InvalidVersionNumber;
}
const parsed = std.fmt.parseInt(u32, message[i..space_position], 10) catch return ParseError.InvalidVersionNumber;
i = space_position + 1;
break :blk parsed;
} else {
return ParseError.InvalidVersionNumber;
};
const verb = if (std.mem.indexOfScalarPos(u8, message, i, ' ')) |space_position| blk: {
if (i == space_position) {
return ParseError.InvalidVerb;
}
const s = message[i..space_position];
for (s) |char| {
if (char < 'A' or char > 'Z') {
return ParseError.InvalidVerb;
}
}
i = space_position + 1;
break :blk s;
} else {
return ParseError.InvalidVerb;
};
const line_delimiter_position = std.mem.indexOfScalarPos(u8, message, i, '\n') orelse message.len;
const service = message[i..line_delimiter_position];
if (service.len == 0) {
return ParseError.InvalidService;
}
if (!std.unicode.utf8ValidateSlice(service)) {
return ParseError.NonUtf8ServiceName;
}
return @This(){
.version = version,
.verb = verb,
.service = service,
.context = HeaderParsingContext{
.start_position = line_delimiter_position + 1,
},
};
}
};
test Metadata {
{
const metadata = try Metadata.parse("MOO/1 REQUEST foo/bar\n");
try std.testing.expectEqual(1, metadata.version);
try std.testing.expectEqualStrings("REQUEST", metadata.verb);
try std.testing.expectEqualStrings("foo/bar", metadata.service);
}
{
const metadata = try Metadata.parse("MOO/20 COMPLETE foo\n");
try std.testing.expectEqual(20, metadata.version);
try std.testing.expectEqualStrings("COMPLETE", metadata.verb);
try std.testing.expectEqualStrings("foo", metadata.service);
}
}
test "(Metadata).parse() should reject invalid signature" {
{
const err = Metadata.parse("");
try std.testing.expectError(Metadata.ParseError.InvalidSignature, err);
}
{
const err = Metadata.parse("\n");
try std.testing.expectError(Metadata.ParseError.InvalidSignature, err);
}
{
const err = Metadata.parse("MOO1 REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidSignature, err);
}
{
const err = Metadata.parse("MEOW/1 REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidSignature, err);
}
}
test "(Metadata).parse() should reject invalid version" {
{
const err = Metadata.parse("MOO/1.0 REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err);
}
{
const err = Metadata.parse("MOO/-1 REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err);
}
{
const err = Metadata.parse("MOO/ REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err);
}
{
const err = Metadata.parse("MOO/0x1 REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err);
}
{
const err = Metadata.parse("MOO/one REQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVersionNumber, err);
}
}
test "(Metadata).parse() should reject invalid verb" {
{
const err = Metadata.parse("MOO/1 request foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVerb, err);
}
{
const err = Metadata.parse("MOO/1 RÉQUEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVerb, err);
}
{
const err = Metadata.parse("MOO/1 REQUEST1 foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVerb, err);
}
{
const err = Metadata.parse("MOO/1 RE❓️UEST foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVerb, err);
}
{
const err = Metadata.parse("MOO/1 \n");
try std.testing.expectError(Metadata.ParseError.InvalidVerb, err);
}
{
const err = Metadata.parse("MOO/1 foo/bar\n");
try std.testing.expectError(Metadata.ParseError.InvalidVerb, err);
}
}
test "(Metadata).parse() should reject invalid service" {
{
const err = Metadata.parse("MOO/1 REQUEST \n");
try std.testing.expectError(Metadata.ParseError.InvalidService, err);
}
{
const err = Metadata.parse("MOO/1 REQUEST ");
try std.testing.expectError(Metadata.ParseError.InvalidService, err);
}
{
const err = Metadata.parse("MOO/1 REQUEST \xc3\x28\n");
try std.testing.expectError(Metadata.ParseError.NonUtf8ServiceName, err);
}
}
const BodyParsingContext = struct {
start_position: usize,
content_type: []const u8,
content_length: usize,
};
/// WellKnownHeaders stores headers defined in the official node-roon api source code and
/// discards every other header fields.
pub const WellKnownHeaders = struct {
/// MIME type.
/// This slice refers to the source message bytes. Accessing this field after
/// free-ing the source message bytes is use-after-free.
content_type: []const u8,
/// Byte size of the body.
content_length: usize,
/// An ID unique to a connection, to associate corresponding request and response.
/// This slice refers to the source message bytes. Accessing this field after
/// free-ing the source message bytes is use-after-free.
request_id: []const u8,
body_start_position: usize,
pub const ParseError = error{};
pub fn parse(message: []const u8, context: HeaderParsingContext) ParseError!@This() {
_ = message;
_ = context;
@panic("Not implemented");
}
pub fn getContext(self: *const @This()) BodyParsingContext {
return .{
.start_position = self.body_start_position,
.content_type = self.content_type,
.content_length = self.content_length,
};
}
};
/// HashMapHeaders
pub const HashMapHeaders = struct {
/// Header key-values.
/// Both key and value points to the source message bytes. Accessing those after
/// free-ing the source message bytes is use-after-free.
map: std.hash_map.StringHashMap([]const u8),
context: BodyParsingContext,
pub const ParseError = std.mem.Allocator.Error;
pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: HeaderParsingContext) ParseError!@This() {
_ = std.hash_map.StringHashMap([]const u8).init(allocator);
_ = message;
_ = context;
@panic("Not implemented");
}
pub fn deinit(self: *@This()) void {
self.map.deinit();
}
};
/// RawBody contains a bytes slice of body section in a MOO message.
/// The bytes field points to the source message bytes: freeing the source message bytes
/// then accessing the bytes field results in use-after-free.
pub const RawBody = struct {
bytes: []const u8,
pub const ParseError = error{};
pub fn parse(message: []const u8, context: BodyParsingContext) ParseError!@This() {
_ = message;
_ = context;
@panic("Not implemented");
}
};
/// JsonBody stores MOO message body as JSON value. User has to provided an expected
/// type (schema) as "T".
pub fn JsonBody(comptime T: type) type {
return struct {
allocator: std.mem.Allocator,
value: *const T,
parsed: std.json.Parsed(T),
pub const ParseError = error{};
pub fn parse(allocator: std.mem.Allocator, message: []const u8, context: BodyParsingContext) ParseError!@This() {
_ = allocator;
_ = message;
_ = context;
@panic("Not implemented");
}
pub fn deinit(self: @This()) void {
self.parsed.deinit();
self.* = undefined;
}
};
}