From fbdff341785ed0f672d315f050429d321ff95a8f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 26 Feb 2025 09:33:50 +0800 Subject: [PATCH 01/13] Make CDP server more authoritative with respect to IDs The TL;DR is that this commit enforces the use of correct IDs, introduces a BrowserContext, and adds some CDP tests. These are the ids we need to be aware of when talking about CDP: - id - browserContextId - targetId - sessionId - loaderId - frameId The `id` is the only one that _should_ originate from the driver. It's attached to most messages and it's how we maintain a request -> response flow: when the server responds to a specific message, it echo's back the id from the requested message. (As opposed to out-of-band events sent from the server which won't have an `id`). When I say "id" from this point forward, I mean every id except for this req->res id. Every other id is created by the browser. Prior to this commit, we didn't really check incoming ids from the driver. If the driver said "attachToTarget" and included a targetId, we just assumed that this was the current targetId. This was aided by the fact that we only used hard-coded IDS. If _we_ only "create" a frameId of "FRAME-1", then it's tempting to think the driver will only ever send a frameId of "FRAME-1". The issue with this approach is that _if_ the browser and driver fall out of sync and there's only ever 1 browserContextId, 1 sessionId and 1 frameId, it's not impossible to imagine cases where we behave on the thing. Imagine this flow: - Driver asks for a new BrowserContext - Browser says OK, your browserContextId is 1 - Driver, for whatever reason, says close browserContextId 2 - Browser says, OK, but it doesn't check the id and just closes the only BrowserContext it knows about (which is 1) By both re-using the same hard-coded ids, and not verifying that the ids sent from the client correspond to the correct ids, any issues are going to be hard to debug. Currently LOADER_ID and FRAEM_ID are still hard-coded. Baby steps. --- src/browser/browser.zig | 6 - src/cdp/browser.zig | 8 +- src/cdp/cdp.zig | 452 ++++++++++++++++++--------- src/cdp/css.zig | 2 +- src/cdp/dom.zig | 36 +-- src/cdp/emulation.zig | 2 +- src/cdp/fetch.zig | 2 +- src/cdp/inspector.zig | 2 +- src/cdp/log.zig | 2 +- src/cdp/network.zig | 2 +- src/cdp/page.zig | 154 +++------- src/cdp/performance.zig | 2 +- src/cdp/runtime.zig | 16 +- src/cdp/security.zig | 2 +- src/cdp/target.zig | 659 +++++++++++++++++++++++++--------------- src/cdp/testing.zig | 298 ++++++++++++++++-- src/id.zig | 12 +- 17 files changed, 1097 insertions(+), 560 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index ec0f39161..7b8756571 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -98,12 +98,6 @@ pub const Browser = struct { self.session = null; } } - - pub fn currentPage(self: *Browser) ?*Page { - if (self.session.page == null) return null; - - return &self.session.page.?; - } }; // Session is like a browser's tab. diff --git a/src/cdp/browser.zig b/src/cdp/browser.zig index 635de3bdf..da972f894 100644 --- a/src/cdp/browser.zig +++ b/src/cdp/browser.zig @@ -33,7 +33,7 @@ pub fn processMessage(cmd: anytype) !void { setDownloadBehavior, getWindowForTarget, setWindowBounds, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .getVersion => return getVersion(cmd), @@ -88,7 +88,6 @@ test "cdp.browser: getVersion" { try ctx.processMessage(.{ .id = 32, - .sessionID = "leto", .method = "Browser.getVersion", }); @@ -99,7 +98,7 @@ test "cdp.browser: getVersion" { .revision = REVISION, .userAgent = USER_AGENT, .jsVersion = JS_VERSION, - }, .{ .id = 32, .index = 0 }); + }, .{ .id = 32, .index = 0, .session_id = null }); } test "cdp.browser: getWindowForTarget" { @@ -108,7 +107,6 @@ test "cdp.browser: getWindowForTarget" { try ctx.processMessage(.{ .id = 33, - .sessionId = "leto", .method = "Browser.getWindowForTarget", }); @@ -116,5 +114,5 @@ test "cdp.browser: getWindowForTarget" { try ctx.expectSentResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{ .windowState = "normal" }, - }, .{ .id = 33, .index = 0, .session_id = "leto" }); + }, .{ .id = 33, .index = 0, .session_id = null }); } diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index dfa7aaa0a..466ff7657 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -20,115 +20,78 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const json = std.json; -const dom = @import("dom.zig"); const Loop = @import("jsruntime").Loop; -// const Client = @import("../server.zig").Client; const asUint = @import("../str/parser.zig").asUint; +const Incrementing = @import("../id.zig").Incrementing; const log = std.log.scoped(.cdp); pub const URL_BASE = "chrome://newtab/"; pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C"; pub const FRAME_ID = "FRAMEIDD8AED408A0467AC93100BCDBE"; -pub const BROWSER_SESSION_ID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0); -pub const CONTEXT_SESSION_ID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4); pub const TimestampEvent = struct { timestamp: f64, }; pub const CDP = CDPT(struct { - const Client = @import("../server.zig").Client; + const Loop = *@import("jsruntime").Loop; + const Client = *@import("../server.zig").Client; const Browser = @import("../browser/browser.zig").Browser; const Session = @import("../browser/browser.zig").Session; }); +const SessionIdGen = Incrementing(u32, "SID"); +const TargetIdGen = Incrementing(u32, "TID"); +const BrowserContextIdGen = Incrementing(u32, "BID"); + // Generic so that we can inject mocks into it. pub fn CDPT(comptime TypeProvider: type) type { return struct { + loop: TypeProvider.Loop, + // Used for sending message to the client and closing on error - client: *TypeProvider.Client, + client: TypeProvider.Client, + + allocator: Allocator, // The active browser - browser: Browser, + browser: ?Browser = null, - // The active browser session - session: ?*Session, + target_id_gen: TargetIdGen = .{}, + session_id_gen: SessionIdGen = .{}, + browser_context_id_gen: BrowserContextIdGen = .{}, - allocator: Allocator, + browser_context: ?BrowserContext(Self), // Re-used arena for processing a message. We're assuming that we're getting // 1 message at a time. message_arena: std.heap.ArenaAllocator, - // State - url: []const u8, - frame_id: []const u8, - loader_id: []const u8, - session_id: SessionID, - context_id: ?[]const u8, - execution_context_id: u32, - security_origin: []const u8, - page_life_cycle_events: bool, - secure_context_type: []const u8, - node_list: dom.NodeList, - node_search_list: dom.NodeSearchList, - const Self = @This(); pub const Browser = TypeProvider.Browser; pub const Session = TypeProvider.Session; - pub fn init(allocator: Allocator, client: *TypeProvider.Client, loop: anytype) Self { + pub fn init(allocator: Allocator, client: TypeProvider.Client, loop: TypeProvider.Loop) Self { return .{ + .loop = loop, .client = client, - .browser = Browser.init(allocator, loop), - .session = null, .allocator = allocator, - .url = URL_BASE, - .execution_context_id = 0, - .context_id = null, - .frame_id = FRAME_ID, - .session_id = .CONTEXTSESSIONID0497A05C95417CF4, - .security_origin = URL_BASE, - .secure_context_type = "Secure", // TODO = enum - .loader_id = LOADER_ID, + .browser_context = null, .message_arena = std.heap.ArenaAllocator.init(allocator), - .page_life_cycle_events = false, // TODO; Target based value - .node_list = dom.NodeList.init(allocator), - .node_search_list = dom.NodeSearchList.init(allocator), }; } pub fn deinit(self: *Self) void { - self.node_list.deinit(); - for (self.node_search_list.items) |*s| { - s.deinit(); + if (self.browser_context) |*bc| { + bc.deinit(); } - self.node_search_list.deinit(); - - self.browser.deinit(); self.message_arena.deinit(); } - pub fn reset(self: *Self) void { - self.node_list.reset(); - - // deinit all node searches. - for (self.node_search_list.items) |*s| { - s.deinit(); - } - self.node_search_list.clearAndFree(); - } - - pub fn newSession(self: *Self) !void { - self.session = try self.browser.newSession(self); - } - pub fn handleMessage(self: *Self, msg: []const u8) bool { - self.processMessage(msg) catch |err| { - log.err("failed to process message: {}\n{s}", .{ err, msg }); - return false; - }; + // if there's an error, it's already been logged + self.processMessage(msg) catch return false; return true; } @@ -140,83 +103,236 @@ pub fn CDPT(comptime TypeProvider: type) type { // Called from above, in processMessage which handles client messages // but can also be called internally. For example, Target.sendMessageToTarget - // calls back into dispatch to capture the response + // calls back into dispatch to capture the response. pub fn dispatch(self: *Self, arena: Allocator, sender: anytype, str: []const u8) !void { const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{ .ignore_unknown_fields = true, }) catch return error.InvalidJSON; - const domain, const action = blk: { - const method = input.method; - const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { - return error.InvalidMethod; - }; - break :blk .{ method[0..i], method[i + 1 ..] }; - }; - var command = Command(Self, @TypeOf(sender)){ - .json = str, + .input = .{ + .json = str, + .id = input.id, + .action = "", + .params = input.params, + .session_id = input.sessionId, + }, .cdp = self, - .id = input.id, .arena = arena, - .action = action, - ._params = input.params, - .session_id = input.sessionId, .sender = sender, - .session = self.session orelse blk: { - try self.newSession(); - break :blk self.session.?; - }, + .browser_context = if (self.browser_context) |*bc| bc else null, + }; + + // See dispatchStartupCommand for more info on this. + var is_startup = false; + if (input.sessionId) |input_session_id| { + if (std.mem.eql(u8, input_session_id, "STARTUP")) { + is_startup = true; + } else if (self.isValidSessionId(input_session_id) == false) { + return command.sendError(-32001, "Unknown sessionId"); + } + } + + if (is_startup) { + dispatchStartupCommand(&command) catch |err| { + command.sendError(-31999, @errorName(err)) catch {}; + return err; + }; + } else { + dispatchCommand(&command, input.method) catch |err| { + command.sendError(-31998, @errorName(err)) catch {}; + return err; + }; + } + } + + // A CDP session isn't 100% fully driven by the driver. There's are + // independent actions that the browser is expected to take. For example + // Puppeteer expects the browser to startup a tab and thus have existing + // targets. + // To this end, we create a [very] dummy BrowserContext, Target and + // Session. There isn't actually a BrowserContext, just a special id. + // When messages are received with the "STARTUP" sessionId, we do + // "special" handling - the bare minimum we need to do until the driver + // switches to a real BrowserContext. + // (I can imagine this logic will become driver-specific) + fn dispatchStartupCommand(command: anytype) !void { + return command.sendResult(null, .{}); + } + + fn dispatchCommand(command: anytype, method: []const u8) !void { + const domain = blk: { + const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse { + return error.InvalidMethod; + }; + command.input.action = method[i + 1 ..]; + break :blk method[0..i]; }; switch (domain.len) { 3 => switch (@as(u24, @bitCast(domain[0..3].*))) { - asUint("DOM") => return @import("dom.zig").processMessage(&command), - asUint("Log") => return @import("log.zig").processMessage(&command), - asUint("CSS") => return @import("css.zig").processMessage(&command), + asUint("DOM") => return @import("dom.zig").processMessage(command), + asUint("Log") => return @import("log.zig").processMessage(command), + asUint("CSS") => return @import("css.zig").processMessage(command), else => {}, }, 4 => switch (@as(u32, @bitCast(domain[0..4].*))) { - asUint("Page") => return @import("page.zig").processMessage(&command), + asUint("Page") => return @import("page.zig").processMessage(command), else => {}, }, 5 => switch (@as(u40, @bitCast(domain[0..5].*))) { - asUint("Fetch") => return @import("fetch.zig").processMessage(&command), + asUint("Fetch") => return @import("fetch.zig").processMessage(command), else => {}, }, 6 => switch (@as(u48, @bitCast(domain[0..6].*))) { - asUint("Target") => return @import("target.zig").processMessage(&command), + asUint("Target") => return @import("target.zig").processMessage(command), else => {}, }, 7 => switch (@as(u56, @bitCast(domain[0..7].*))) { - asUint("Browser") => return @import("browser.zig").processMessage(&command), - asUint("Runtime") => return @import("runtime.zig").processMessage(&command), - asUint("Network") => return @import("network.zig").processMessage(&command), + asUint("Browser") => return @import("browser.zig").processMessage(command), + asUint("Runtime") => return @import("runtime.zig").processMessage(command), + asUint("Network") => return @import("network.zig").processMessage(command), else => {}, }, 8 => switch (@as(u64, @bitCast(domain[0..8].*))) { - asUint("Security") => return @import("security.zig").processMessage(&command), + asUint("Security") => return @import("security.zig").processMessage(command), else => {}, }, 9 => switch (@as(u72, @bitCast(domain[0..9].*))) { - asUint("Emulation") => return @import("emulation.zig").processMessage(&command), - asUint("Inspector") => return @import("inspector.zig").processMessage(&command), + asUint("Emulation") => return @import("emulation.zig").processMessage(command), + asUint("Inspector") => return @import("inspector.zig").processMessage(command), else => {}, }, 11 => switch (@as(u88, @bitCast(domain[0..11].*))) { - asUint("Performance") => return @import("performance.zig").processMessage(&command), + asUint("Performance") => return @import("performance.zig").processMessage(command), else => {}, }, else => {}, } + return error.UnknownDomain; } + fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool { + const browser_context = &(self.browser_context orelse return false); + const session_id = browser_context.session_id orelse return false; + return std.mem.eql(u8, session_id, input_session_id); + } + + pub fn createBrowserContext(self: *Self) ![]const u8 { + if (self.browser_context != null) { + return error.AlreadyExists; + } + const browser_context_id = self.browser_context_id_gen.next(); + + // is this safe? + self.browser_context = undefined; + errdefer self.browser_context = null; + try BrowserContext(Self).init(&self.browser_context.?, browser_context_id, self); + + return browser_context_id; + } + + pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool { + const bc = &(self.browser_context orelse return false); + if (std.mem.eql(u8, bc.id, browser_context_id) == false) { + return false; + } + bc.deinit(); + self.browser_context = null; + return true; + } + fn sendJSON(self: *Self, message: anytype) !void { return self.client.sendJSON(message, .{ .emit_null_optional_fields = false, }); } + }; +} + +pub fn BrowserContext(comptime CDP_T: type) type { + const dom = @import("dom.zig"); + + return struct { + id: []const u8, + cdp: *CDP_T, + + browser: CDP_T.Browser, + // Represents the browser session. There is no equivalent in CDP. For + // all intents and purpose, from CDP's point of view our Browser and + // our Session more or less maps to a BrowserContext. THIS HAS ZERO + // RELATION TO SESSION_ID + session: *CDP_T.Session, + + // Maps to our Page. (There are other types of targets, but we only + // deal with "pages" for now). Since we only allow 1 open page at a + // time, we only have 1 target_id. + target_id: ?[]const u8, + + // The CDP session_id. After the target/page is created, the client + // "attaches" to it (either explicitly or automatically). We return a + // "sessionId" which identifies this link. `sessionId` is the how + // the CDP client informs us what it's trying to manipulate. Because we + // only support 1 BrowserContext at a time, and 1 page at a time, this + // is all pretty straightforward, but it still needs to be enforced, i.e. + // if we get a request with a sessionId that doesn't match the current one + // we should reject it. + session_id: ?[]const u8, + + // State + url: []const u8, + frame_id: []const u8, + loader_id: []const u8, + security_origin: []const u8, + page_life_cycle_events: bool, + secure_context_type: []const u8, + node_list: dom.NodeList, + node_search_list: dom.NodeSearchList, + + const Self = @This(); + + fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void { + self.* = .{ + .id = id, + .cdp = cdp, + .browser = undefined, + .session = undefined, + .target_id = null, + .session_id = null, + .url = URL_BASE, + .frame_id = FRAME_ID, + .security_origin = URL_BASE, + .secure_context_type = "Secure", // TODO = enum + .loader_id = LOADER_ID, + .page_life_cycle_events = false, // TODO; Target based value + .node_list = dom.NodeList.init(cdp.allocator), + .node_search_list = dom.NodeSearchList.init(cdp.allocator), + }; + + self.browser = CDP_T.Browser.init(cdp.allocator, cdp.loop); + errdefer self.browser.deinit(); + self.session = try self.browser.newSession(self); + } + + pub fn deinit(self: *Self) void { + self.node_list.deinit(); + for (self.node_search_list.items) |*s| { + s.deinit(); + } + self.node_search_list.deinit(); + self.browser.deinit(); + } + + pub fn reset(self: *Self) void { + self.node_list.reset(); + + // deinit all node searches. + for (self.node_search_list.items) |*s| { + s.deinit(); + } + self.node_search_list.clearAndFree(); + } pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { if (std.log.defaultLogEnabled(.debug)) { @@ -252,19 +368,24 @@ pub fn CDPT(comptime TypeProvider: type) type { }; } - // This is hacky * 2. First, we have the JSON payload by gluing our + // This is hacky x 2. First, we create the JSON payload by gluing our // session_id onto it. Second, we're much more client/websocket aware than // we should be. fn sendInspectorMessage(self: *Self, msg: []const u8) !void { - var arena = std.heap.ArenaAllocator.init(self.allocator); + const session_id = self.session_id orelse { + // We no longer have an active session. What should we do + // in this case? + return; + }; + + const cdp = self.cdp; + var arena = std.heap.ArenaAllocator.init(cdp.allocator); errdefer arena.deinit(); const field = ",\"sessionId\":\""; - const session_id = @tagName(self.session_id); // + 1 for the closing quote after the session id // + 10 for the max websocket header - const message_len = msg.len + session_id.len + 1 + field.len + 10; var buf: std.ArrayListUnmanaged(u8) = .{}; @@ -283,7 +404,7 @@ pub fn CDPT(comptime TypeProvider: type) type { buf.appendSliceAssumeCapacity("\"}"); std.debug.assert(buf.items.len == message_len); - try self.client.sendJSONRaw(arena, buf); + try cdp.client.sendJSONRaw(arena, buf); } }; } @@ -294,38 +415,29 @@ pub fn CDPT(comptime TypeProvider: type) type { // generic. pub fn Command(comptime CDP_T: type, comptime Sender: type) type { return struct { - // reference to our CDP instance - cdp: *CDP_T, - - // Comes directly from the input.id field - id: ?i64, - // A misc arena that can be used for any allocation for processing // the message arena: Allocator, - // the browser session - session: *CDP_T.Session, - - // The "action" of the message.Given a method of "LOG.enable", the - // action is "enable" - action: []const u8, - - // Comes directly from the input.sessionId field - session_id: ?[]const u8, + // reference to our CDP instance + cdp: *CDP_T, - // Unparsed / untyped input.params. - _params: ?InputParams, + // The browser context this command targets + browser_context: ?*BrowserContext(CDP_T), - // The full raw json input - json: []const u8, + // The command input (the id, optional session_id, params, ...) + input: Input, + // In most cases, Sender is going to be cdp itself. We'll call + // sender.sendJSON() and CDP will send it to the client. But some + // comamnds are dispatched internally, in which cases the Sender will + // be code to capture the data that we were "sending". sender: Sender, const Self = @This(); pub fn params(self: *const Self, comptime T: type) !?T { - if (self._params) |p| { + if (self.input.params) |p| { return try json.parseFromSliceLeaky( T, self.arena, @@ -336,20 +448,26 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type { return null; } + pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) { + _ = try self.cdp.createBrowserContext(); + self.browser_context = &self.cdp.browser_context.?; + return self.browser_context.?; + } + const SendResultOpts = struct { include_session_id: bool = true, }; pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void { return self.sender.sendJSON(.{ - .id = self.id, + .id = self.input.id, .result = if (comptime @typeInfo(@TypeOf(result)) == .Null) struct {}{} else result, - .sessionId = if (opts.include_session_id) self.session_id else null, + .sessionId = if (opts.include_session_id) self.input.session_id else null, }); } + const SendEventOpts = struct { session_id: ?[]const u8 = null, }; - pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void { // Events ALWAYS go to the client. self.sender should not be used return self.cdp.sendJSON(.{ @@ -358,6 +476,32 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type { .sessionId = opts.session_id, }); } + + pub fn sendError(self: *Self, code: i32, message: []const u8) !void { + return self.sender.sendJSON(.{ + .id = self.input.id, + .code = code, + .message = message, + }); + } + + const Input = struct { + // When we reply to a message, we echo back the message id + id: ?i64, + + // The "action" of the message.Given a method of "LOG.enable", the + // action is "enable" + action: []const u8, + + // See notes in BrowserContext about session_id + session_id: ?[]const u8, + + // Unparsed / untyped input.params. + params: ?InputParams, + + // The full raw json input + json: []const u8, + }; }; } @@ -395,24 +539,7 @@ const InputParams = struct { } }; -// Common -// ------ - -// TODO: hard coded IDs -pub const SessionID = enum { - BROWSERSESSIONID597D9875C664CAC0, - CONTEXTSESSIONID0497A05C95417CF4, - - pub fn parse(str: []const u8) !SessionID { - return std.meta.stringToEnum(SessionID, str) orelse { - log.err("parse sessionID: {s}", .{str}); - return error.InvalidSessionID; - }; - } -}; - const testing = @import("testing.zig"); - test "cdp: invalid json" { var ctx = testing.context(); defer ctx.deinit(); @@ -425,6 +552,7 @@ test "cdp: invalid json" { try testing.expectError(error.InvalidMethod, ctx.processMessage(.{ .method = "Target", })); + try ctx.expectSentError(-31998, "InvalidMethod", .{}); try testing.expectError(error.UnknownDomain, ctx.processMessage(.{ .method = "Unknown.domain", @@ -434,3 +562,53 @@ test "cdp: invalid json" { .method = "Target.over9000", })); } + +test "cdp: invalid sessionId" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + // we have no browser context + try ctx.processMessage(.{ .method = "Hi", .sessionId = "nope" }); + try ctx.expectSentError(-32001, "Unknown sessionId", .{}); + } + + { + // we have a brower context but no session_id + _ = try ctx.loadBrowserContext(.{}); + try ctx.processMessage(.{ .method = "Hi", .sessionId = "BC-Has-No-SessionId" }); + try ctx.expectSentError(-32001, "Unknown sessionId", .{}); + } + + { + // we have a brower context with a different session_id + _ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" }); + try ctx.processMessage(.{ .method = "Hi", .sessionId = "SESS-1" }); + try ctx.expectSentError(-32001, "Unknown sessionId", .{}); + } +} + +test "cdp: STARTUP sessionId" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + // we have no browser context + try ctx.processMessage(.{ .id = 2, .method = "Hi", .sessionId = "STARTUP" }); + try ctx.expectSentResult(null, .{ .id = 2, .index = 0, .session_id = "STARTUP" }); + } + + { + // we have a brower context but no session_id + _ = try ctx.loadBrowserContext(.{}); + try ctx.processMessage(.{ .id = 3, .method = "Hi", .sessionId = "STARTUP" }); + try ctx.expectSentResult(null, .{ .id = 3, .index = 0, .session_id = "STARTUP" }); + } + + { + // we have a brower context with a different session_id + _ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" }); + try ctx.processMessage(.{ .id = 4, .method = "Hi", .sessionId = "STARTUP" }); + try ctx.expectSentResult(null, .{ .id = 4, .index = 0, .session_id = "STARTUP" }); + } +} diff --git a/src/cdp/css.zig b/src/cdp/css.zig index 21834d839..4dc4c001f 100644 --- a/src/cdp/css.zig +++ b/src/cdp/css.zig @@ -22,7 +22,7 @@ const cdp = @import("cdp.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/dom.zig b/src/cdp/dom.zig index 2bcacd416..ce4d2610e 100644 --- a/src/cdp/dom.zig +++ b/src/cdp/dom.zig @@ -29,7 +29,7 @@ pub fn processMessage(cmd: anytype) !void { performSearch, getSearchResults, discardSearchResults, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), @@ -133,14 +133,13 @@ fn getDocument(cmd: anytype) !void { // pierce: ?bool = null, // })) orelse return error.InvalidParams; - // retrieve the root node - const page = cmd.session.page orelse return error.NoPage; - const doc = page.doc orelse return error.NoDocument; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const page = bc.session.currentPage() orelse return error.PageNotLoaded; + const doc = page.doc orelse return error.DocumentNotLoaded; - const state = cmd.cdp; const node = parser.documentToNode(doc); - var n = try Node.init(node, &state.node_list); - _ = try n.initChildren(cmd.arena, node, &state.node_list); + var n = try Node.init(node, &bc.node_list); + _ = try n.initChildren(cmd.arena, node, &bc.node_list); return cmd.sendResult(.{ .root = n, @@ -184,21 +183,20 @@ fn performSearch(cmd: anytype) !void { includeUserAgentShadowDOM: ?bool = null, })) orelse return error.InvalidParams; - // retrieve the root node - const page = cmd.session.page orelse return error.NoPage; - const doc = page.doc orelse return error.NoDocument; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const page = bc.session.currentPage() orelse return error.PageNotLoaded; + const doc = page.doc orelse return error.DocumentNotLoaded; const list = try css.querySelectorAll(cmd.cdp.allocator, parser.documentToNode(doc), params.query); const ln = list.nodes.items.len; var ns = try NodeSearch.initCapacity(cmd.cdp.allocator, ln); - var state = cmd.cdp; for (list.nodes.items) |n| { - const id = try state.node_list.set(n); + const id = try bc.node_list.set(n); try ns.append(id); } - try state.node_search_list.append(ns); + try bc.node_search_list.append(ns); return cmd.sendResult(.{ .searchId = ns.name, @@ -212,13 +210,14 @@ fn discardSearchResults(cmd: anytype) !void { searchId: []const u8, })) orelse return error.InvalidParams; - var state = cmd.cdp; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + // retrieve the search from context - for (state.node_search_list.items, 0..) |*s, i| { + for (bc.node_search_list.items, 0..) |*s, i| { if (!std.mem.eql(u8, s.name, params.searchId)) continue; s.deinit(); - _ = state.node_search_list.swapRemove(i); + _ = bc.node_search_list.swapRemove(i); break; } @@ -237,10 +236,11 @@ fn getSearchResults(cmd: anytype) !void { return error.BadIndices; } - const state = cmd.cdp; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + // retrieve the search from context var ns: ?*const NodeSearch = undefined; - for (state.node_search_list.items) |s| { + for (bc.node_search_list.items) |s| { if (!std.mem.eql(u8, s.name, params.searchId)) continue; ns = &s; break; diff --git a/src/cdp/emulation.zig b/src/cdp/emulation.zig index 88c5ddf72..9edc0d6fd 100644 --- a/src/cdp/emulation.zig +++ b/src/cdp/emulation.zig @@ -26,7 +26,7 @@ pub fn processMessage(cmd: anytype) !void { setFocusEmulationEnabled, setDeviceMetricsOverride, setTouchEmulationEnabled, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .setEmulatedMedia => return setEmulatedMedia(cmd), diff --git a/src/cdp/fetch.zig b/src/cdp/fetch.zig index 0a9a8cae4..00a2f948e 100644 --- a/src/cdp/fetch.zig +++ b/src/cdp/fetch.zig @@ -22,7 +22,7 @@ const cdp = @import("cdp.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { disable, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .disable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/inspector.zig b/src/cdp/inspector.zig index 21834d839..4dc4c001f 100644 --- a/src/cdp/inspector.zig +++ b/src/cdp/inspector.zig @@ -22,7 +22,7 @@ const cdp = @import("cdp.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/log.zig b/src/cdp/log.zig index 21834d839..4dc4c001f 100644 --- a/src/cdp/log.zig +++ b/src/cdp/log.zig @@ -22,7 +22,7 @@ const cdp = @import("cdp.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/network.zig b/src/cdp/network.zig index 60d9cbbbc..ac5200163 100644 --- a/src/cdp/network.zig +++ b/src/cdp/network.zig @@ -23,7 +23,7 @@ pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, setCacheDisabled, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/page.zig b/src/cdp/page.zig index 6ca9655dd..3dfb2c440 100644 --- a/src/cdp/page.zig +++ b/src/cdp/page.zig @@ -28,7 +28,7 @@ pub fn processMessage(cmd: anytype) !void { addScriptToEvaluateOnNewDocument, createIsolatedWorld, navigate, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), @@ -56,43 +56,16 @@ const Frame = struct { }; fn getFrameTree(cmd: anytype) !void { - // output - const FrameTree = struct { - frameTree: struct { - frame: Frame, - }, - childFrames: ?[]@This() = null, - - pub fn format( - self: @This(), - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("cdp.page.getFrameTree { "); - try writer.writeAll(".frameTree = { "); - try writer.writeAll(".frame = { "); - - const frame = self.frameTree.frame; - try writer.writeAll(".id = "); - try std.fmt.formatText(frame.id, "s", options, writer); - try writer.writeAll(", .loaderId = "); - try std.fmt.formatText(frame.loaderId, "s", options, writer); - try writer.writeAll(", .url = "); - try std.fmt.formatText(frame.url, "s", options, writer); - try writer.writeAll(" } } }"); - } - }; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const state = cmd.cdp; - return cmd.sendResult(FrameTree{ + return cmd.sendResult(.{ .frameTree = .{ - .frame = .{ - .id = state.frame_id, - .url = state.url, - .securityOrigin = state.security_origin, - .secureContextType = state.secure_context_type, - .loaderId = state.loader_id, + .frame = Frame{ + .url = bc.url, + .id = bc.frame_id, + .loaderId = bc.loader_id, + .securityOrigin = bc.security_origin, + .secureContextType = bc.secure_context_type, }, }, }, .{}); @@ -103,7 +76,8 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void { // enabled: bool, // })) orelse return error.InvalidParams; - cmd.cdp.page_life_cycle_events = true; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + bc.page_life_cycle_events = true; return cmd.sendResult(null, .{}); } @@ -116,27 +90,16 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { // runImmediately: bool = false, // })) orelse return error.InvalidParams; - const Response = struct { - identifier: []const u8 = "1", - - pub fn format( - self: @This(), - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("cdp.page.addScriptToEvaluateOnNewDocument { "); - try writer.writeAll(".identifier = "); - try std.fmt.formatText(self.identifier, "s", options, writer); - try writer.writeAll(" }"); - } - }; - return cmd.sendResult(Response{}, .{}); + return cmd.sendResult(.{ + .identifier = "1", + }, .{}); } // TODO: hard coded method fn createIsolatedWorld(cmd: anytype) !void { - const session_id = cmd.session_id orelse return error.SessionIdRequired; + _ = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + const session_id = cmd.input.session_id orelse return error.SessionIdRequired; const params = (try cmd.params(struct { frameId: []const u8, @@ -166,7 +129,16 @@ fn createIsolatedWorld(cmd: anytype) !void { } fn navigate(cmd: anytype) !void { - const session_id = cmd.session_id orelse return error.SessionIdRequired; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + // didn't create? + _ = bc.target_id orelse return error.TargetIdNotLoaded; + + // didn't attach? + const session_id = bc.session_id orelse return error.SessionIdNotLoaded; + + // if we have a target_id we have to have a page; + std.debug.assert(bc.session.page != null); const params = (try cmd.params(struct { url: []const u8, @@ -177,12 +149,11 @@ fn navigate(cmd: anytype) !void { })) orelse return error.InvalidParams; // change state - var state = cmd.cdp; - state.reset(); - state.url = params.url; + bc.reset(); + bc.url = params.url; // TODO: hard coded ID - state.loader_id = "AF8667A203C5392DBE9AC290044AA4C2"; + bc.loader_id = "AF8667A203C5392DBE9AC290044AA4C2"; const LifecycleEvent = struct { frameId: []const u8, @@ -192,8 +163,8 @@ fn navigate(cmd: anytype) !void { }; var life_event = LifecycleEvent{ - .frameId = state.frame_id, - .loaderId = state.loader_id, + .frameId = bc.frame_id, + .loaderId = bc.loader_id, .name = "init", .timestamp = 343721.796037, }; @@ -201,39 +172,17 @@ fn navigate(cmd: anytype) !void { // frameStartedLoading event // TODO: event partially hard coded try cmd.sendEvent("Page.frameStartedLoading", .{ - .frameId = state.frame_id, + .frameId = bc.frame_id, }, .{ .session_id = session_id }); - if (state.page_life_cycle_events) { + if (bc.page_life_cycle_events) { try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); } // output - const Response = struct { - frameId: []const u8, - loaderId: ?[]const u8, - errorText: ?[]const u8 = null, - - pub fn format( - self: @This(), - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("cdp.page.navigate.Resp { "); - try writer.writeAll(".frameId = "); - try std.fmt.formatText(self.frameId, "s", options, writer); - if (self.loaderId) |loaderId| { - try writer.writeAll(", .loaderId = '"); - try std.fmt.formatText(loaderId, "s", options, writer); - } - try writer.writeAll(" }"); - } - }; - - try cmd.sendResult(Response{ - .frameId = state.frame_id, - .loaderId = state.loader_id, + try cmd.sendResult(.{ + .frameId = bc.frame_id, + .loaderId = bc.loader_id, }, .{}); // TODO: at this point do we need async the following actions to be async? @@ -242,24 +191,21 @@ fn navigate(cmd: anytype) !void { // TODO: noop event, we have no env context at this point, is it necesarry? try cmd.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); - // Launch navigate, the page must have been created by a - // target.createTarget. - var p = cmd.session.currentPage() orelse return error.NoPage; - state.execution_context_id += 1; - const aux_data = try std.fmt.allocPrint( cmd.arena, // NOTE: we assume this is the default web page "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", - .{state.frame_id}, + .{bc.frame_id}, ); - try p.navigate(params.url, aux_data); + + var page = bc.session.currentPage().?; + try page.navigate(params.url, aux_data); // Events // lifecycle init event // TODO: partially hard coded - if (state.page_life_cycle_events) { + if (bc.page_life_cycle_events) { life_event.name = "init"; life_event.timestamp = 343721.796037; try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); @@ -271,11 +217,11 @@ fn navigate(cmd: anytype) !void { try cmd.sendEvent("Page.frameNavigated", .{ .type = "Navigation", .frame = Frame{ - .id = state.frame_id, - .url = state.url, - .securityOrigin = state.security_origin, - .secureContextType = state.secure_context_type, - .loaderId = state.loader_id, + .id = bc.frame_id, + .url = bc.url, + .securityOrigin = bc.security_origin, + .secureContextType = bc.secure_context_type, + .loaderId = bc.loader_id, }, }, .{ .session_id = session_id }); @@ -289,7 +235,7 @@ fn navigate(cmd: anytype) !void { // lifecycle DOMContentLoaded event // TODO: partially hard coded - if (state.page_life_cycle_events) { + if (bc.page_life_cycle_events) { life_event.name = "DOMContentLoaded"; life_event.timestamp = 343721.803338; try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); @@ -305,7 +251,7 @@ fn navigate(cmd: anytype) !void { // lifecycle DOMContentLoaded event // TODO: partially hard coded - if (state.page_life_cycle_events) { + if (bc.page_life_cycle_events) { life_event.name = "load"; life_event.timestamp = 343721.824655; try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); @@ -313,6 +259,6 @@ fn navigate(cmd: anytype) !void { // frameStoppedLoading return cmd.sendEvent("Page.frameStoppedLoading", .{ - .frameId = state.frame_id, + .frameId = bc.frame_id, }, .{ .session_id = session_id }); } diff --git a/src/cdp/performance.zig b/src/cdp/performance.zig index 8db70ed4d..d06bebfae 100644 --- a/src/cdp/performance.zig +++ b/src/cdp/performance.zig @@ -23,7 +23,7 @@ const asUint = @import("../str/parser.zig").asUint; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/runtime.zig b/src/cdp/runtime.zig index 3da661055..d34920ea5 100644 --- a/src/cdp/runtime.zig +++ b/src/cdp/runtime.zig @@ -27,7 +27,7 @@ pub fn processMessage(cmd: anytype) !void { addBinding, callFunctionOn, releaseObject, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .runIfWaitingForDebugger => return cmd.sendResult(null, .{}), @@ -41,26 +41,24 @@ fn sendInspector(cmd: anytype, action: anytype) !void { try logInspector(cmd, action); } - if (cmd.session_id) |s| { - cmd.cdp.session_id = try cdp.SessionID.parse(s); - } + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; // remove awaitPromise true params // TODO: delete when Promise are correctly handled by zig-js-runtime if (action == .callFunctionOn or action == .evaluate) { - const json = cmd.json; + const json = cmd.input.json; if (std.mem.indexOf(u8, json, "\"awaitPromise\":true")) |_| { // +1 because we'll be turning a true -> false const buf = try cmd.arena.alloc(u8, json.len + 1); _ = std.mem.replace(u8, json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf); - cmd.session.callInspector(buf); + bc.session.callInspector(buf); return; } } - cmd.session.callInspector(cmd.json); + bc.session.callInspector(cmd.input.json); - if (cmd.id != null) { + if (cmd.input.id != null) { return cmd.sendResult(null, .{}); } } @@ -110,7 +108,7 @@ fn logInspector(cmd: anytype, action: anytype) !void { }, else => return, }; - const id = cmd.id orelse return error.RequiredId; + const id = cmd.input.id orelse return error.RequiredId; const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id}); var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{}); diff --git a/src/cdp/security.zig b/src/cdp/security.zig index 21834d839..4dc4c001f 100644 --- a/src/cdp/security.zig +++ b/src/cdp/security.zig @@ -22,7 +22,7 @@ const cdp = @import("cdp.zig"); pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { enable, - }, cmd.action) orelse return error.UnknownMethod; + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { .enable => return cmd.sendResult(null, .{}), diff --git a/src/cdp/target.zig b/src/cdp/target.zig index 815741d9f..9e0087a78 100644 --- a/src/cdp/target.zig +++ b/src/cdp/target.zig @@ -17,290 +17,167 @@ // along with this program. If not, see . const std = @import("std"); -const cdp = @import("cdp.zig"); const log = std.log.scoped(.cdp); // TODO: hard coded IDs -const CONTEXT_ID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89"; -const PAGE_TARGET_ID = "PAGETARGETIDB638E9DC0F52DDC"; -const BROWSER_TARGET_ID = "browser9-targ-et6f-id0e-83f3ab73a30c"; -const BROWER_CONTEXT_ID = "BROWSERCONTEXTIDA95049E9DFE95EA9"; -const TARGET_ID = "TARGETID460A8F29706A2ADF14316298"; const LOADER_ID = "LOADERID42AA389647D702B4D805F49A"; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { - setDiscoverTargets, - setAutoAttach, attachToTarget, - getTargetInfo, - getBrowserContexts, + closeTarget, createBrowserContext, - disposeBrowserContext, createTarget, - closeTarget, - sendMessageToTarget, detachFromTarget, - }, cmd.action) orelse return error.UnknownMethod; + disposeBrowserContext, + getBrowserContexts, + getTargetInfo, + sendMessageToTarget, + setAutoAttach, + setDiscoverTargets, + }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { - .setDiscoverTargets => return setDiscoverTargets(cmd), - .setAutoAttach => return setAutoAttach(cmd), .attachToTarget => return attachToTarget(cmd), - .getTargetInfo => return getTargetInfo(cmd), - .getBrowserContexts => return getBrowserContexts(cmd), + .closeTarget => return closeTarget(cmd), .createBrowserContext => return createBrowserContext(cmd), - .disposeBrowserContext => return disposeBrowserContext(cmd), .createTarget => return createTarget(cmd), - .closeTarget => return closeTarget(cmd), - .sendMessageToTarget => return sendMessageToTarget(cmd), .detachFromTarget => return detachFromTarget(cmd), + .disposeBrowserContext => return disposeBrowserContext(cmd), + .getBrowserContexts => return getBrowserContexts(cmd), + .getTargetInfo => return getTargetInfo(cmd), + .sendMessageToTarget => return sendMessageToTarget(cmd), + .setAutoAttach => return setAutoAttach(cmd), + .setDiscoverTargets => return setDiscoverTargets(cmd), } } -// TODO: noop method -fn setDiscoverTargets(cmd: anytype) !void { - return cmd.sendResult(null, .{}); -} - -const AttachToTarget = struct { - sessionId: []const u8, - targetInfo: TargetInfo, - waitingForDebugger: bool = false, -}; - -const TargetCreated = struct { - sessionId: []const u8, - targetInfo: TargetInfo, -}; - -const TargetInfo = struct { - targetId: []const u8, - type: []const u8 = "page", - title: []const u8, - url: []const u8, - attached: bool = true, - canAccessOpener: bool = false, - browserContextId: []const u8, -}; - -// TODO: noop method -fn setAutoAttach(cmd: anytype) !void { - // const TargetFilter = struct { - // type: ?[]const u8 = null, - // exclude: ?bool = null, - // }; - - // const params = (try cmd.params(struct { - // autoAttach: bool, - // waitForDebuggerOnStart: bool, - // flatten: bool = true, - // filter: ?[]TargetFilter = null, - // })) orelse return error.InvalidParams; - - // attachedToTarget event - if (cmd.session_id == null) { - try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ - .sessionId = cdp.BROWSER_SESSION_ID, - .targetInfo = .{ - .targetId = PAGE_TARGET_ID, - .title = "about:blank", - .url = cdp.URL_BASE, - .browserContextId = BROWER_CONTEXT_ID, - }, - }, .{}); - } - - return cmd.sendResult(null, .{}); -} - -// TODO: noop method -fn attachToTarget(cmd: anytype) !void { - const params = (try cmd.params(struct { - targetId: []const u8, - flatten: bool = true, - })) orelse return error.InvalidParams; - - // attachedToTarget event - if (cmd.session_id == null) { - try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ - .sessionId = cdp.BROWSER_SESSION_ID, - .targetInfo = .{ - .targetId = params.targetId, - .title = "about:blank", - .url = cdp.URL_BASE, - .browserContextId = BROWER_CONTEXT_ID, - }, - }, .{}); - } - - return cmd.sendResult( - .{ .sessionId = cmd.session_id orelse cdp.BROWSER_SESSION_ID }, - .{ .include_session_id = false }, - ); -} - -fn getTargetInfo(cmd: anytype) !void { - // const params = (try cmd.params(struct { - // targetId: ?[]const u8 = null, - // })) orelse return error.InvalidParams; - return cmd.sendResult(.{ - .targetId = BROWSER_TARGET_ID, - .type = "browser", - .title = "", - .url = "", - .attached = true, - .canAccessOpener = false, - }, .{ .include_session_id = false }); -} - -// Browser context are not handled and not in the roadmap for now -// The following methods are "fake" - -// TODO: noop method fn getBrowserContexts(cmd: anytype) !void { - var context_ids: []const []const u8 = undefined; - if (cmd.cdp.context_id) |context_id| { - context_ids = &.{context_id}; + var browser_context_ids: []const []const u8 = undefined; + if (cmd.browser_context) |bc| { + browser_context_ids = &.{bc.id}; } else { - context_ids = &.{}; + browser_context_ids = &.{}; } return cmd.sendResult(.{ - .browserContextIds = context_ids, + .browserContextIds = browser_context_ids, }, .{ .include_session_id = false }); } -// TODO: noop method fn createBrowserContext(cmd: anytype) !void { - // const params = (try cmd.params(struct { - // disposeOnDetach: bool = false, - // proxyServer: ?[]const u8 = null, - // proxyBypassList: ?[]const u8 = null, - // originsWithUniversalNetworkAccess: ?[][]const u8 = null, - // })) orelse return error.InvalidParams; - - cmd.cdp.context_id = CONTEXT_ID; - - const Response = struct { - browserContextId: []const u8, - - pub fn format( - self: @This(), - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("cdp.target.createBrowserContext { "); - try writer.writeAll(".browserContextId = "); - try std.fmt.formatText(self.browserContextId, "s", options, writer); - try writer.writeAll(" }"); - } + const bc = cmd.createBrowserContext() catch |err| switch (err) { + error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time"), + else => return err, }; - return cmd.sendResult(Response{ - .browserContextId = CONTEXT_ID, + return cmd.sendResult(.{ + .browserContextId = bc.id, }, .{}); } fn disposeBrowserContext(cmd: anytype) !void { - // const params = (try cmd.params(struct { - // browserContextId: []const u8, - // proxyServer: ?[]const u8 = null, - // proxyBypassList: ?[]const u8 = null, - // originsWithUniversalNetworkAccess: ?[][]const u8 = null, - // })) orelse return error.InvalidParams; + const params = (try cmd.params(struct { + browserContextId: []const u8, + })) orelse return error.InvalidParams; - try cmd.cdp.newSession(); + if (cmd.cdp.disposeBrowserContext(params.browserContextId) == false) { + return cmd.sendError(-32602, "No browser context with the given id found"); + } try cmd.sendResult(null, .{}); } fn createTarget(cmd: anytype) !void { const params = (try cmd.params(struct { - url: []const u8, - width: ?u64 = null, - height: ?u64 = null, + // url: []const u8, + // width: ?u64 = null, + // height: ?u64 = null, browserContextId: ?[]const u8 = null, - enableBeginFrameControl: bool = false, - newWindow: bool = false, - background: bool = false, - forTab: ?bool = null, + // enableBeginFrameControl: bool = false, + // newWindow: bool = false, + // background: bool = false, + // forTab: ?bool = null, })) orelse return error.InvalidParams; - // change CDP state - var state = cmd.cdp; - state.frame_id = TARGET_ID; - state.url = "about:blank"; - state.security_origin = "://"; - state.secure_context_type = "InsecureScheme"; - state.loader_id = LOADER_ID; - - if (cmd.session_id) |s| { - state.session_id = try cdp.SessionID.parse(s); + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + if (bc.target_id != null) { + return error.TargetAlreadyLoaded; } - - // TODO stop the previous page instead? - if (cmd.session.page != null) { - return error.pageAlreadyExists; + if (params.browserContextId) |param_browser_context_id| { + if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) { + return error.UnknownBrowserContextId; + } } - // create the page - const p = try cmd.session.createPage(); - state.execution_context_id += 1; + // if target_id is null, we should never have a page + std.debug.assert(bc.session.page == null); + + // if target_id is null, we should never have a session_id + std.debug.assert(bc.session_id == null); + + const page = try bc.session.createPage(); + const target_id = cmd.cdp.target_id_gen.next(); + + // change CDP state + bc.url = "about:blank"; + bc.security_origin = "://"; + bc.secure_context_type = "InsecureScheme"; + bc.loader_id = LOADER_ID; // start the js env const aux_data = try std.fmt.allocPrint( cmd.arena, // NOTE: we assume this is the default web page "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", - .{state.frame_id}, + .{target_id}, ); - try p.start(aux_data); + try page.start(aux_data); - const browser_context_id = params.browserContextId orelse CONTEXT_ID; + try cmd.sendResult(.{ + .targetId = target_id, + }, .{}); // send targetCreated event - try cmd.sendEvent("Target.targetCreated", TargetCreated{ - .sessionId = cdp.CONTEXT_SESSION_ID, - .targetInfo = .{ - .targetId = state.frame_id, + // TODO: should this only be sent when Target.setDiscoverTargets + // has been enabled? + try cmd.sendEvent("Target.targetCreated", .{ + .targetInfo = TargetInfo{ + .url = bc.url, + .targetId = target_id, .title = "about:blank", - .url = state.url, - .browserContextId = browser_context_id, - .attached = true, + .browserContextId = bc.id, + .attached = false, }, - }, .{ .session_id = cmd.session_id }); + }, .{}); - // send attachToTarget event - try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ - .sessionId = cdp.CONTEXT_SESSION_ID, - .waitingForDebugger = true, - .targetInfo = .{ - .targetId = state.frame_id, - .title = "about:blank", - .url = state.url, - .browserContextId = browser_context_id, - .attached = true, - }, - }, .{ .session_id = cmd.session_id }); - - const Response = struct { - targetId: []const u8 = TARGET_ID, - - pub fn format( - self: @This(), - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try writer.writeAll("cdp.target.createTarget { "); - try writer.writeAll(".targetId = "); - try std.fmt.formatText(self.targetId, "s", options, writer); - try writer.writeAll(" }"); - } - }; - return cmd.sendResult(Response{}, .{}); + // only if setAutoAttach is true? + try doAttachtoTarget(cmd, target_id); + bc.target_id = target_id; +} + +fn attachToTarget(cmd: anytype) !void { + const params = (try cmd.params(struct { + targetId: []const u8, + flatten: bool = true, + })) orelse return error.InvalidParams; + + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const target_id = bc.target_id orelse return error.TargetNotLoaded; + if (std.mem.eql(u8, target_id, params.targetId) == false) { + return error.UnknownTargetId; + } + + if (bc.session_id != null) { + return error.SessionAlreadyLoaded; + } + + try doAttachtoTarget(cmd, target_id); + + return cmd.sendResult( + .{ .sessionId = bc.session_id }, + .{ .include_session_id = false }, + ); } fn closeTarget(cmd: anytype) !void { @@ -308,27 +185,67 @@ fn closeTarget(cmd: anytype) !void { targetId: []const u8, })) orelse return error.InvalidParams; - try cmd.sendResult(.{ - .success = true, - }, .{ .include_session_id = false }); + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const target_id = bc.target_id orelse return error.TargetNotLoaded; + if (std.mem.eql(u8, target_id, params.targetId) == false) { + return error.UnknownTargetId; + } - const session_id = cmd.session_id orelse cdp.CONTEXT_SESSION_ID; + // can't be null if we have a target_id + std.debug.assert(bc.session.page != null); - // Inspector.detached event - try cmd.sendEvent("Inspector.detached", .{ - .reason = "Render process gone.", - }, .{ .session_id = session_id }); + try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false }); - // detachedFromTarget event - try cmd.sendEvent("Target.detachedFromTarget", .{ - .sessionId = session_id, - .targetId = params.targetId, - .reason = "Render process gone.", - }, .{}); + // could be null, created but never attached + if (bc.session_id) |session_id| { + // Inspector.detached event + try cmd.sendEvent("Inspector.detached", .{ + .reason = "Render process gone.", + }, .{ .session_id = session_id }); - if (cmd.session.page) |*page| { - page.end(); + // detachedFromTarget event + try cmd.sendEvent("Target.detachedFromTarget", .{ + .targetId = target_id, + .sessionId = session_id, + .reason = "Render process gone.", + }, .{}); + + bc.session_id = null; } + + bc.session.currentPage().?.end(); + bc.target_id = null; +} + +fn getTargetInfo(cmd: anytype) !void { + const params = (try cmd.params(struct { + targetId: ?[]const u8 = null, + })) orelse return error.InvalidParams; + + if (params.targetId) |param_target_id| { + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const target_id = bc.target_id orelse return error.TargetNotLoaded; + if (std.mem.eql(u8, target_id, param_target_id) == false) { + return error.UnknownTargetId; + } + + return cmd.sendResult(.{ + .targetId = target_id, + .type = "page", + .title = "", + .url = "", + .attached = true, + .canAccessOpener = false, + }, .{ .include_session_id = false }); + } + + return cmd.sendResult(.{ + .type = "browser", + .title = "", + .url = "", + .attached = true, + .canAccessOpener = false, + }, .{ .include_session_id = false }); } fn sendMessageToTarget(cmd: anytype) !void { @@ -337,6 +254,18 @@ fn sendMessageToTarget(cmd: anytype) !void { sessionId: []const u8, })) orelse return error.InvalidParams; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + if (bc.target_id == null) { + return error.TargetNotLoaded; + } + + std.debug.assert(bc.session_id != null); + if (std.mem.eql(u8, bc.session_id.?, params.sessionId) == false) { + // Is this right? Is the params.sessionId meant to be the active + // sessionId? What else could it be? We have no other session_id. + return error.UnknownSessionId; + } + const Capture = struct { allocator: std.mem.Allocator, buf: std.ArrayListUnmanaged(u8), @@ -354,7 +283,7 @@ fn sendMessageToTarget(cmd: anytype) !void { }; cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| { - log.err("send message {d} ({s}): {any}", .{ cmd.id orelse -1, params.message, err }); + log.err("send message {d} ({s}): {any}", .{ cmd.input.id orelse -1, params.message, err }); return err; }; @@ -368,3 +297,253 @@ fn sendMessageToTarget(cmd: anytype) !void { fn detachFromTarget(cmd: anytype) !void { return cmd.sendResult(null, .{}); } + +// TODO: noop method +fn setDiscoverTargets(cmd: anytype) !void { + return cmd.sendResult(null, .{}); +} + +fn setAutoAttach(cmd: anytype) !void { + // const params = (try cmd.params(struct { + // autoAttach: bool, + // waitForDebuggerOnStart: bool, + // flatten: bool = true, + // filter: ?[]TargetFilter = null, + // })) orelse return error.InvalidParams; + + // TODO: should set a flag to send Target.attachedToTarget events + + try cmd.sendResult(null, .{}); + + if (cmd.browser_context) |bc| { + if (bc.target_id == null) { + // hasn't attached yet + const target_id = cmd.cdp.target_id_gen.next(); + try doAttachtoTarget(cmd, target_id); + bc.target_id = target_id; + } + // should we send something here? + return; + } + + // This is a hack. Puppeteer, and probably others, expect the Browser to + // automatically started creating targets. Things like an empty tab, or + // a blank page. And they block until this happens. So we send an event + // telling them that they've been attached to our Broswer. Hopefully, the + // first thing they'll do is create a real BrowserContext and progress from + // there. + // This hack requires the main cdp dispatch handler to special case + // messages from this "STARTUP" session. + try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ + .sessionId = "STARTUP", + .targetInfo = TargetInfo{ + .type = "browser", + .targetId = "TID-STARTUP", + .title = "about:blank", + .url = "chrome://newtab/", + .browserContextId = "BID-STARTUP", + }, + }, .{}); +} + +fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void { + const bc = cmd.browser_context.?; + std.debug.assert(bc.session_id == null); + const session_id = cmd.cdp.session_id_gen.next(); + + try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{ + .sessionId = session_id, + .targetInfo = TargetInfo{ + .targetId = target_id, + .title = "about:blank", + .url = "chrome://newtab/", + .browserContextId = bc.id, + }, + }, .{}); + + bc.session_id = session_id; +} + +const AttachToTarget = struct { + sessionId: []const u8, + targetInfo: TargetInfo, + waitingForDebugger: bool = false, +}; + +const TargetInfo = struct { + url: []const u8, + title: []const u8, + targetId: []const u8, + attached: bool = true, + type: []const u8 = "page", + canAccessOpener: bool = false, + browserContextId: []const u8, +}; + +const testing = @import("testing.zig"); +test "cdp.target: getBrowserContexts" { + var ctx = testing.context(); + defer ctx.deinit(); + + // { + // // no browser context + // try ctx.processMessage(.{.id = 4, .method = "Target.getBrowserContexts"}); + + // try ctx.expectSentResult(.{ + // .browserContextIds = &.{}, + // }, .{ .id = 4, .session_id = null }); + // } + + { + // with a browser context + _ = try ctx.loadBrowserContext(.{ .id = "BID-X" }); + try ctx.processMessage(.{ .id = 5, .method = "Target.getBrowserContexts" }); + + try ctx.expectSentResult(.{ + .browserContextIds = &.{"BID-X"}, + }, .{ .id = 5, .session_id = null }); + } +} + +test "cdp.target: createBrowserContext" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + try ctx.processMessage(.{ .id = 4, .method = "Target.createBrowserContext" }); + try ctx.expectSentResult(.{ + .browserContextId = ctx.cdp().browser_context.?.id, + }, .{ .id = 4, .session_id = null }); + } + + { + // we already have one now + try ctx.processMessage(.{ .id = 5, .method = "Target.createBrowserContext" }); + try ctx.expectSentError(-32000, "Cannot have more than one browser context at a time", .{ .id = 5 }); + } +} + +test "cdp.target: disposeBrowserContext" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + try testing.expectError(error.InvalidParams, ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" })); + try ctx.expectSentError(-31998, "InvalidParams", .{ .id = 7 }); + } + + { + try ctx.processMessage(.{ + .id = 8, + .method = "Target.disposeBrowserContext", + .params = .{ .browserContextId = "BID-10" }, + }); + try ctx.expectSentError(-32602, "No browser context with the given id found", .{ .id = 8 }); + } + + { + _ = try ctx.loadBrowserContext(.{ .id = "BID-20" }); + try ctx.processMessage(.{ + .id = 9, + .method = "Target.disposeBrowserContext", + .params = .{ .browserContextId = "BID-20" }, + }); + try ctx.expectSentResult(null, .{ .id = 9 }); + try testing.expectEqual(null, ctx.cdp().browser_context); + } +} + +test "cdp.target: createTarget" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ + .id = 10, + .method = "Target.createTarget", + .params = struct {}{}, + })); + try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); + } + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); + { + try testing.expectError(error.UnknownBrowserContextId, ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } })); + try ctx.expectSentError(-31998, "UnknownBrowserContextId", .{ .id = 10 }); + } + + { + try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } }); + try testing.expectEqual(true, bc.target_id != null); + try testing.expectString( + \\{"isDefault":true,"type":"default","frameId":"TID-1"} + , bc.session.page.?.aux_data); + + try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 }); + try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{}); + + try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{}); + } +} + +test "cdp.target: closeTarget" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } })); + try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); + } + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); + { + try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 }); + } + + // pretend we createdTarget first + _ = try bc.session.createPage(); + bc.target_id = "TID-A"; + { + try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); + } + + { + try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-A" } }); + try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 }); + try testing.expectEqual(null, bc.session.page); + try testing.expectEqual(null, bc.target_id); + } +} + +test "cdp.target: attachToTarget" { + var ctx = testing.context(); + defer ctx.deinit(); + + { + try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } })); + try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); + } + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); + { + try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 }); + } + + // pretend we createdTarget first + _ = try bc.session.createPage(); + bc.target_id = "TID-B"; + { + try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); + } + + { + try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-B" } }); + const session_id = bc.session_id.?; + try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 }); + try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{}); + } +} diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 889b8c1fd..1e7bc25cd 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator; const Testing = @This(); -const cdp = @import("cdp.zig"); +const main = @import("cdp.zig"); const parser = @import("netsurf"); pub const expectEqual = std.testing.expectEqual; @@ -13,32 +13,57 @@ pub const expectError = std.testing.expectError; pub const expectString = std.testing.expectEqualStrings; const Browser = struct { - session: ?Session = null, + session: ?*Session = null, + arena: std.heap.ArenaAllocator, - pub fn init(_: Allocator, loop: anytype) Browser { + pub fn init(allocator: Allocator, loop: anytype) Browser { _ = loop; - return .{}; + return .{ + .arena = std.heap.ArenaAllocator.init(allocator), + }; } - pub fn deinit(_: *const Browser) void {} + pub fn deinit(self: *Browser) void { + self.arena.deinit(); + } pub fn newSession(self: *Browser, ctx: anytype) !*Session { _ = ctx; + if (self.session != null) { + return error.MockBrowserSessionAlreadyExists; + } - self.session = .{}; - return &self.session.?; + const allocator = self.arena.allocator(); + self.session = try allocator.create(Session); + self.session.?.* = .{ + .page = null, + .allocator = allocator, + }; + return self.session.?; + } + + pub fn hasSession(self: *const Browser, session_id: []const u8) bool { + const session = self.session orelse return false; + return std.mem.eql(u8, session.id, session_id); } }; const Session = struct { page: ?Page = null, + allocator: Allocator, pub fn currentPage(self: *Session) ?*Page { return &(self.page orelse return null); } pub fn createPage(self: *Session) !*Page { - self.page = .{}; + if (self.page != null) { + return error.MockBrowserPageAlreadyExists; + } + self.page = .{ + .session = self, + .allocator = self.allocator, + }; return &self.page.?; } @@ -49,6 +74,9 @@ const Session = struct { }; const Page = struct { + session: *Session, + allocator: Allocator, + aux_data: []const u8 = "", doc: ?*parser.Document = null, pub fn navigate(self: *Page, url: []const u8, aux_data: []const u8) !void { @@ -58,18 +86,18 @@ const Page = struct { } pub fn start(self: *Page, aux_data: []const u8) !void { - _ = self; - _ = aux_data; + self.aux_data = try self.allocator.dupe(u8, aux_data); } pub fn end(self: *Page) void { - _ = self; + self.session.page = null; } }; const Client = struct { allocator: Allocator, - sent: std.ArrayListUnmanaged([]const u8) = .{}, + sent: std.ArrayListUnmanaged(json.Value) = .{}, + serialized: std.ArrayListUnmanaged([]const u8) = .{}, fn init(allocator: Allocator) Client { return .{ @@ -78,15 +106,21 @@ const Client = struct { } pub fn sendJSON(self: *Client, message: anytype, opts: json.StringifyOptions) !void { - const serialized = try json.stringifyAlloc(self.allocator, message, opts); - try self.sent.append(self.allocator, serialized); + var opts_copy = opts; + opts_copy.whitespace = .indent_2; + const serialized = try json.stringifyAlloc(self.allocator, message, opts_copy); + try self.serialized.append(self.allocator, serialized); + + const value = try json.parseFromSliceLeaky(json.Value, self.allocator, serialized, .{}); + try self.sent.append(self.allocator, value); } }; -const TestCDP = cdp.CDPT(struct { +const TestCDP = main.CDPT(struct { + pub const Loop = void; pub const Browser = Testing.Browser; pub const Session = Testing.Session; - pub const Client = Testing.Client; + pub const Client = *Testing.Client; }); const TestContext = struct { @@ -106,15 +140,39 @@ const TestContext = struct { self.client = Client.init(self.arena.allocator()); // Don't use the arena here. We want to detect leaks in CDP. // The arena is only for test-specific stuff - self.cdp_ = TestCDP.init(std.testing.allocator, &self.client.?, "dummy-loop"); + self.cdp_ = TestCDP.init(std.testing.allocator, &self.client.?, {}); } return &self.cdp_.?; } + const BrowserContextOpts = struct { + id: ?[]const u8 = null, + session_id: ?[]const u8 = null, + }; + pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) { + var c = self.cdp(); + if (c.browser_context) |*bc| { + bc.deinit(); + c.browser_context = null; + } + + _ = try c.createBrowserContext(); + var bc = &c.browser_context.?; + + if (opts.id) |id| { + bc.id = id; + } + + if (opts.session_id) |sid| { + bc.session_id = sid; + } + return bc; + } + pub fn processMessage(self: *TestContext, msg: anytype) !void { var json_message: []const u8 = undefined; if (@typeInfo(@TypeOf(msg)) != .Pointer) { - json_message = try std.json.stringifyAlloc(self.arena.allocator(), msg, .{}); + json_message = try json.stringifyAlloc(self.arena.allocator(), msg, .{}); } else { // assume this is a string we want to send as-is, if it isn't, we'll // get a compile error, so no big deal. @@ -132,34 +190,71 @@ const TestContext = struct { index: ?usize = null, session_id: ?[]const u8 = null, }; - pub fn expectSentResult(self: *TestContext, expected: anytype, opts: ExpectResultOpts) !void { const expected_result = .{ .id = opts.id, - .result = expected, + .result = if (comptime @typeInfo(@TypeOf(expected)) == .Null) struct {}{} else expected, .sessionId = opts.session_id, }; - const serialized = try json.stringifyAlloc(self.arena.allocator(), expected_result, .{ + try self.expectSent(expected_result, .{ .index = opts.index }); + } + + const ExpectEventOpts = struct { + index: ?usize = null, + session_id: ?[]const u8 = null, + }; + pub fn expectSentEvent(self: *TestContext, method: []const u8, params: anytype, opts: ExpectEventOpts) !void { + const expected_event = .{ + .method = method, + .params = if (comptime @typeInfo(@TypeOf(params)) == .Null) struct {}{} else params, + .sessionId = opts.session_id, + }; + + try self.expectSent(expected_event, .{ .index = opts.index }); + } + + const ExpectErrorOpts = struct { + id: ?usize = null, + index: ?usize = null, + }; + pub fn expectSentError(self: *TestContext, code: i32, message: []const u8, opts: ExpectErrorOpts) !void { + const expected_message = .{ + .id = opts.id, + .code = code, + .message = message, + }; + try self.expectSent(expected_message, .{ .index = opts.index }); + } + + const SentOpts = struct { + index: ?usize = null, + }; + pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void { + const serialized = try json.stringifyAlloc(self.arena.allocator(), expected, .{ + .whitespace = .indent_2, .emit_null_optional_fields = false, }); for (self.client.?.sent.items, 0..) |sent, i| { - if (std.mem.eql(u8, sent, serialized) == false) { + if (try compareExpectedToSent(serialized, sent) == false) { continue; } + if (opts.index) |expected_index| { if (expected_index != i) { - return error.MessageAtWrongIndex; + return error.ErrorAtWrongIndex; } - return; } + _ = self.client.?.sent.orderedRemove(i); + _ = self.client.?.serialized.orderedRemove(i); + return; } - std.debug.print("Message not found. Expecting:\n{s}\n\nGot:\n", .{serialized}); - for (self.client.?.sent.items, 0..) |sent, i| { + std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized}); + for (self.client.?.serialized.items, 0..) |sent, i| { std.debug.print("#{d}\n{s}\n\n", .{ i, sent }); } - return error.MessageNotFound; + return error.ErrorNotFound; } }; @@ -168,3 +263,152 @@ pub fn context() TestContext { .arena = std.heap.ArenaAllocator.init(std.testing.allocator), }; } + +// Zig makes this hard. When sendJSON is called, we're sending an anytype. +// We can't record that in an ArrayList(???), so we serialize it to JSON. +// Now, ideally, we could just take our expected structure, serialize it to +// json and check if the two are equal. +// Except serializing to JSON isn't deterministic. +// So we serialize the JSON then we deserialize to json.Value. And then we can +// compare our anytype expection with the json.Value that we captured + +fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool { + const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{}); + defer expected_value.deinit(); + return compareJsonValues(expected_value.value, actual); +} + +fn compareJsonValues(a: std.json.Value, b: std.json.Value) bool { + if (!std.mem.eql(u8, @tagName(a), @tagName(b))) { + return false; + } + + switch (a) { + .null => return true, + .bool => return a.bool == b.bool, + .integer => return a.integer == b.integer, + .float => return a.float == b.float, + .number_string => return std.mem.eql(u8, a.number_string, b.number_string), + .string => return std.mem.eql(u8, a.string, b.string), + .array => { + const a_len = a.array.items.len; + const b_len = b.array.items.len; + if (a_len != b_len) { + return false; + } + for (a.array.items, b.array.items) |a_item, b_item| { + if (compareJsonValues(a_item, b_item) == false) { + return false; + } + } + return true; + }, + .object => { + var it = a.object.iterator(); + while (it.next()) |entry| { + const key = entry.key_ptr.*; + if (b.object.get(key)) |b_item| { + if (compareJsonValues(entry.value_ptr.*, b_item) == false) { + return false; + } + } else { + return false; + } + } + return true; + }, + } +} + +// fn compareAnyToJsonValue(expected: anytype, actual: json.Value) bool { +// switch (@typeInfo(@TypeOf(expected))) { +// .Optional => { +// if (expected) |e| { +// return compareAnyToJsonValue(e, actual); +// } +// return actual == .null; +// }, +// .Int, .ComptimeInt => { +// if (actual != .integer) { +// return false; +// } +// return expected == actual.integer; +// }, +// .Float, .ComptimeFloat => { +// if (actual != .float) { +// return false; +// } +// return expected == actual.float; +// }, +// .Bool => { +// if (actual != .bool) { +// return false; +// } +// return expected == actual.bool; +// }, +// .Pointer => |ptr| switch (ptr.size) { +// .One => switch (@typeInfo(ptr.child)) { +// .Struct => return compareAnyToJsonValue(expected.*, actual), +// .Array => |arr| if (arr.child == u8) { +// if (actual != .string) { +// return false; +// } +// return std.mem.eql(u8, expected, actual.string); +// }, +// else => {}, +// }, +// .Slice => switch (ptr.child) { +// u8 => { +// if (actual != .string) { +// return false; +// } +// return std.mem.eql(u8, expected, actual.string); +// }, +// else => {}, +// }, +// else => {}, +// }, +// .Struct => |s| { +// if (s.is_tuple) { +// // how an array might look in an anytype +// if (actual != .array) { +// return false; +// } +// if (s.fields.len != actual.array.items.len) { +// return false; +// } + +// inline for (s.fields, 0..) |f, i| { +// const e = @field(expected, f.name); +// if (compareAnyToJsonValue(e, actual.array.items[i]) == false) { +// return false; +// } +// } +// return true; +// } + +// if (s.fields.len == 0) { +// return (actual == .array and actual.array.items.len == 0); +// } + +// if (actual != .object) { +// return false; +// } +// inline for (s.fields) |f| { +// const e = @field(expected, f.name); +// if (actual.object.get(f.name)) |a| { +// if (compareAnyToJsonValue(e, a) == false) { +// return false; +// } +// } else if (@typeInfo(f.type) != .Optional or e != null) { +// // We don't JSON serialize nulls. So if we're expecting +// // a null, that should show up as a missing field. +// return false; +// } +// } +// return true; +// }, +// else => {}, +// } +// @compileError("Can't compare " ++ @typeName(@TypeOf(expected))); +// } diff --git a/src/id.zig b/src/id.zig index 04f160183..f21af7783 100644 --- a/src/id.zig +++ b/src/id.zig @@ -9,7 +9,7 @@ const std = @import("std"); // - while incrementor is valid // - until the next call to next() // On the positive, it's zero allocation -fn Incrementing(comptime T: type, comptime prefix: []const u8) type { +pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type { // +1 for the '-' separator const NUMERIC_START = prefix.len + 1; const MAX_BYTES = NUMERIC_START + switch (T) { @@ -35,15 +35,15 @@ fn Incrementing(comptime T: type, comptime prefix: []const u8) type { const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*); return struct { - current: T = 0, + counter: T = 0, buffer: [MAX_BYTES]u8 = buffer, const Self = @This(); pub fn next(self: *Self) []const u8 { - const current = self.current; - const n = current +% 1; - defer self.current = n; + const counter = self.counter; + const n = counter +% 1; + defer self.counter = n; const size = std.fmt.formatIntBuf(self.buffer[NUMERIC_START..], n, 10, .lower, .{}); return self.buffer[0 .. NUMERIC_START + size]; @@ -106,7 +106,7 @@ test "id: Incrementing.next" { try testing.expectEqualStrings("IDX-3", id.next()); // force a wrap - id.current = 65533; + id.counter = 65533; try testing.expectEqualStrings("IDX-65534", id.next()); try testing.expectEqualStrings("IDX-65535", id.next()); try testing.expectEqualStrings("IDX-0", id.next()); From e3858b38238061c41945773462e8f98514f16b19 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 4 Mar 2025 12:57:25 +0800 Subject: [PATCH 02/13] send attach events before result --- src/cdp/target.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cdp/target.zig b/src/cdp/target.zig index 9e0087a78..dc0cd1fa0 100644 --- a/src/cdp/target.zig +++ b/src/cdp/target.zig @@ -134,10 +134,6 @@ fn createTarget(cmd: anytype) !void { ); try page.start(aux_data); - try cmd.sendResult(.{ - .targetId = target_id, - }, .{}); - // send targetCreated event // TODO: should this only be sent when Target.setDiscoverTargets // has been enabled? @@ -154,6 +150,10 @@ fn createTarget(cmd: anytype) !void { // only if setAutoAttach is true? try doAttachtoTarget(cmd, target_id); bc.target_id = target_id; + + try cmd.sendResult(.{ + .targetId = target_id, + }, .{}); } fn attachToTarget(cmd: anytype) !void { From 7ed69251e6651f0f0b231db4e8d3107222eb4172 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 4 Mar 2025 13:19:15 +0800 Subject: [PATCH 03/13] allow Target.getTargetInfo to be called without parameters --- src/cdp/target.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cdp/target.zig b/src/cdp/target.zig index dc0cd1fa0..1c278f3a6 100644 --- a/src/cdp/target.zig +++ b/src/cdp/target.zig @@ -218,9 +218,10 @@ fn closeTarget(cmd: anytype) !void { } fn getTargetInfo(cmd: anytype) !void { - const params = (try cmd.params(struct { + const Params = struct { targetId: ?[]const u8 = null, - })) orelse return error.InvalidParams; + }; + const params = (try cmd.params(Params)) orelse Params{}; if (params.targetId) |param_target_id| { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; From ca51b411a14e86939bc45fdf4e718d0fb9fadd56 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 4 Mar 2025 14:11:03 +0800 Subject: [PATCH 04/13] Don't send CDP result when message is forward to inspector. Rely on inspector to send the result, otherwise we'll send 2 responses to the same message (one ourselves and one from the inspector), which Playwright does not like. --- src/cdp/runtime.zig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/cdp/runtime.zig b/src/cdp/runtime.zig index d34920ea5..c054521e8 100644 --- a/src/cdp/runtime.zig +++ b/src/cdp/runtime.zig @@ -57,10 +57,6 @@ fn sendInspector(cmd: anytype, action: anytype) !void { } bc.session.callInspector(cmd.input.json); - - if (cmd.input.id != null) { - return cmd.sendResult(null, .{}); - } } pub const ExecutionContextCreated = struct { From c5456105c1cb5d09af57dad1a0d455a52fc2e2e9 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 3 Mar 2025 15:37:43 +0100 Subject: [PATCH 05/13] upgrade vendor/zig-js-runtime --- vendor/zig-js-runtime | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/zig-js-runtime b/vendor/zig-js-runtime index 61c71e5e3..944c5f0a2 160000 --- a/vendor/zig-js-runtime +++ b/vendor/zig-js-runtime @@ -1 +1 @@ -Subproject commit 61c71e5e390316786a0c780d9135a45890bda846 +Subproject commit 944c5f0a260a04aee284a0013d459e29c8151c2e From 47961c1e63b390bab8a99b73a49b038a825fd184 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 4 Mar 2025 11:29:17 +0100 Subject: [PATCH 06/13] upgrade vendor/zig-async-io --- vendor/zig-async-io | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/zig-async-io b/vendor/zig-async-io index 570f436c7..073546a97 160000 --- a/vendor/zig-async-io +++ b/vendor/zig-async-io @@ -1 +1 @@ -Subproject commit 570f436c7252c2742b0bacb2f7b1a2c879c1e6d3 +Subproject commit 073546a975c437491dad2a16fdcd87c2a7db9c73 From 48ecfb639bb0b1f6107e27067a0465ce574a6485 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 26 Feb 2025 20:43:40 +0800 Subject: [PATCH 07/13] Add Set-Cookie parsing --- src/datetime.zig | 2086 ++++++++++++++++++++++++++++++++++++++++ src/storage/cookie.zig | 421 ++++++++ src/testing.zig | 134 +++ src/unit_tests.zig | 2 + 4 files changed, 2643 insertions(+) create mode 100644 src/datetime.zig create mode 100644 src/storage/cookie.zig create mode 100644 src/testing.zig diff --git a/src/datetime.zig b/src/datetime.zig new file mode 100644 index 000000000..d54ff3309 --- /dev/null +++ b/src/datetime.zig @@ -0,0 +1,2086 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; + +pub const Date = struct { + year: i16, + month: u8, + day: u8, + + pub const Format = enum { + iso8601, + rfc3339, + }; + + pub fn init(year: i16, month: u8, day: u8) !Date { + if (!Date.valid(year, month, day)) { + return error.InvalidDate; + } + + return .{ + .year = year, + .month = month, + .day = day, + }; + } + + pub fn valid(year: i16, month: u8, day: u8) bool { + if (month == 0 or month > 12) { + return false; + } + + if (day == 0) { + return false; + } + + const month_days = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + const max_days = if (month == 2 and (@rem(year, 400) == 0 or (@rem(year, 100) != 0 and @rem(year, 4) == 0))) 29 else month_days[month - 1]; + if (day > max_days) { + return false; + } + + return true; + } + + pub fn parse(input: []const u8, fmt: Format) !Date { + var parser = Parser.init(input); + + const date = switch (fmt) { + .rfc3339 => try parser.rfc3339Date(), + .iso8601 => try parser.iso8601Date(), + }; + + if (parser.unconsumed() != 0) { + return error.InvalidDate; + } + + return date; + } + + pub fn order(a: Date, b: Date) std.math.Order { + const year_order = std.math.order(a.year, b.year); + if (year_order != .eq) return year_order; + + const month_order = std.math.order(a.month, b.month); + if (month_order != .eq) return month_order; + + return std.math.order(a.day, b.day); + } + + pub fn format(self: Date, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void { + var buf: [11]u8 = undefined; + const n = writeDate(&buf, self); + try out.writeAll(buf[0..n]); + } + + pub fn jsonStringify(self: Date, out: anytype) !void { + // Our goal here isn't to validate the date. It's to write what we have + // in a YYYY-MM-DD format. If the data in Date isn't valid, that's not + // our problem and we don't guarantee any reasonable output in such cases. + + // std.fmt.formatInt is difficult to work with. The padding with signs + // doesn't work and it'll always put a + sign given a signed integer with padding + // So, for year, we always feed it an unsigned number (which avoids both issues) + // and prepend the - if we need it.s + var buf: [13]u8 = undefined; + const n = writeDate(buf[1..12], self); + buf[0] = '"'; + buf[n + 1] = '"'; + try out.print("{s}", .{buf[0 .. n + 2]}); + } + + pub fn jsonParse(allocator: Allocator, source: anytype, options: anytype) !Date { + _ = options; + + switch (try source.nextAlloc(allocator, .alloc_if_needed)) { + inline .string, .allocated_string => |str| return Date.parse(str, .rfc3339) catch return error.InvalidCharacter, + else => return error.UnexpectedToken, + } + } +}; + +pub const Time = struct { + hour: u8, + min: u8, + sec: u8, + micros: u32, + + pub const Format = enum { + rfc3339, + }; + + pub fn init(hour: u8, min: u8, sec: u8, micros: u32) !Time { + if (!Time.valid(hour, min, sec, micros)) { + return error.InvalidTime; + } + + return .{ + .hour = hour, + .min = min, + .sec = sec, + .micros = micros, + }; + } + + pub fn valid(hour: u8, min: u8, sec: u8, micros: u32) bool { + if (hour > 23) { + return false; + } + + if (min > 59) { + return false; + } + + if (sec > 59) { + return false; + } + + if (micros > 999999) { + return false; + } + + return true; + } + + pub fn parse(input: []const u8, fmt: Format) !Time { + var parser = Parser.init(input); + const time = switch (fmt) { + .rfc3339 => try parser.time(true), + }; + + if (parser.unconsumed() != 0) { + return error.InvalidTime; + } + return time; + } + + pub fn order(a: Time, b: Time) std.math.Order { + const hour_order = std.math.order(a.hour, b.hour); + if (hour_order != .eq) return hour_order; + + const min_order = std.math.order(a.min, b.min); + if (min_order != .eq) return min_order; + + const sec_order = std.math.order(a.sec, b.sec); + if (sec_order != .eq) return sec_order; + + return std.math.order(a.micros, b.micros); + } + + pub fn format(self: Time, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void { + var buf: [15]u8 = undefined; + const n = writeTime(&buf, self); + try out.writeAll(buf[0..n]); + } + + pub fn jsonStringify(self: Time, out: anytype) !void { + // Our goal here isn't to validate the time. It's to write what we have + // in a hh:mm:ss.sss format. If the data in Time isn't valid, that's not + // our problem and we don't guarantee any reasonable output in such cases. + var buf: [17]u8 = undefined; + const n = writeTime(buf[1..16], self); + buf[0] = '"'; + buf[n + 1] = '"'; + try out.print("{s}", .{buf[0 .. n + 2]}); + } + + pub fn jsonParse(allocator: Allocator, source: anytype, options: anytype) !Time { + _ = options; + + switch (try source.nextAlloc(allocator, .alloc_if_needed)) { + inline .string, .allocated_string => |str| return Time.parse(str, .rfc3339) catch return error.InvalidCharacter, + else => return error.UnexpectedToken, + } + } +}; + +pub const DateTime = struct { + micros: i64, + + const MICROSECONDS_IN_A_DAY = 86_400_000_000; + const MICROSECONDS_IN_AN_HOUR = 3_600_000_000; + const MICROSECONDS_IN_A_MIN = 60_000_000; + const MICROSECONDS_IN_A_SEC = 1_000_000; + + pub const Format = enum { + rfc822, + rfc3339, + }; + + pub const TimestampPrecision = enum { + seconds, + milliseconds, + microseconds, + }; + + pub const TimeUnit = enum { + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + }; + + // https://blog.reverberate.org/2020/05/12/optimizing-date-algorithms.html + pub fn initUTC(year: i16, month: u8, day: u8, hour: u8, min: u8, sec: u8, micros: u32) !DateTime { + if (Date.valid(year, month, day) == false) { + return error.InvalidDate; + } + + if (Time.valid(hour, min, sec, micros) == false) { + return error.InvalidTime; + } + + const year_base = 4800; + const month_adj = @as(i32, @intCast(month)) - 3; // March-based month + const carry: u8 = if (month_adj < 0) 1 else 0; + const adjust: u8 = if (carry == 1) 12 else 0; + const year_adj: i64 = year + year_base - carry; + const month_days = @divTrunc(((month_adj + adjust) * 62719 + 769), 2048); + const leap_days = @divTrunc(year_adj, 4) - @divTrunc(year_adj, 100) + @divTrunc(year_adj, 400); + + const date_micros: i64 = (year_adj * 365 + leap_days + month_days + (day - 1) - 2472632) * MICROSECONDS_IN_A_DAY; + const time_micros = (@as(i64, @intCast(hour)) * MICROSECONDS_IN_AN_HOUR) + (@as(i64, @intCast(min)) * MICROSECONDS_IN_A_MIN) + (@as(i64, @intCast(sec)) * MICROSECONDS_IN_A_SEC) + micros; + + return fromUnix(date_micros + time_micros, .microseconds); + } + + pub fn fromUnix(value: i64, precision: TimestampPrecision) !DateTime { + switch (precision) { + .seconds => { + if (value < -210863520000 or value > 253402300799) { + return error.OutsideJulianPeriod; + } + return .{ .micros = value * 1_000_000 }; + }, + .milliseconds => { + if (value < -210863520000000 or value > 253402300799999) { + return error.OutsideJulianPeriod; + } + return .{ .micros = value * 1_000 }; + }, + .microseconds => { + if (value < -210863520000000000 or value > 253402300799999999) { + return error.OutsideJulianPeriod; + } + return .{ .micros = value }; + }, + } + } + + pub fn now() DateTime { + return .{ + .micros = std.time.microTimestamp(), + }; + } + + pub fn parse(input: []const u8, fmt: Format) !DateTime { + switch (fmt) { + .rfc822 => return parseRFC822(input), + .rfc3339 => return parseRFC3339(input), + } + } + + pub fn parseRFC822(input: []const u8) !DateTime { + if (input.len < 10) { + return error.InvalidDateTime; + } + var parser = Parser.init(input); + if (input[3] == ',' and input[4] == ' ') { + _ = std.meta.stringToEnum(enum { Mon, Tue, Wed, Thu, Fri, Sat, Sun }, input[0..3]) orelse return error.InvalidDate; + // skip over the "DoW, " + parser.pos = 5; + } + + const day = parser.paddedInt(u8, 2) orelse return error.InvalidDate; + if (parser.consumeIf(' ') == false) { + return error.InvalidDate; + } + + const month = std.meta.stringToEnum(enum { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }, parser.consumeN(3) orelse return error.InvalidDate) orelse return error.InvalidDate; + + if (parser.consumeIf(' ') == false) { + return error.InvalidDate; + } + + const year = parser.paddedInt(i16, 4) orelse blk: { + const short_year = parser.paddedInt(u8, 2) orelse return error.InvalidDate; + break :blk if (short_year > 68) 1900 + @as(i16, short_year) else 2000 + @as(i16, short_year); + }; + + if (parser.consumeIf(' ') == false) { + return error.InvalidDateTime; + } + const tm = try parser.time(false); + + if (parser.consumeIf(' ') == false) { + return error.InvalidTime; + } + + _ = std.meta.stringToEnum(enum { UT, GMT, Z }, parser.rest()) orelse return error.UnsupportedTimeZone; + + return initUTC(year, @intFromEnum(month) + 1, day, tm.hour, tm.min, tm.sec, tm.micros); + } + + pub fn parseRFC3339(input: []const u8) !DateTime { + var parser = Parser.init(input); + + const dt = try parser.rfc3339Date(); + + const year = dt.year; + if (year < -4712 or year > 9999) { + return error.OutsideJulianPeriod; + } + + // Per the spec, it can be argued thatt 't' and even ' ' should be allowed, + // but certainly not encouraged. + if (parser.consumeIf('T') == false) { + return error.InvalidDateTime; + } + + const tm = try parser.time(true); + + switch (parser.unconsumed()) { + 0 => return error.InvalidDateTime, + 1 => if (parser.consumeIf('Z') == false) { + return error.InvalidDateTime; + }, + 6 => { + const suffix = parser.rest(); + if (suffix[0] != '+' and suffix[0] != '-') { + return error.InvalidDateTime; + } + if (std.mem.eql(u8, suffix[1..], "00:00") == false) { + return error.NonUTCNotSupported; + } + }, + else => return error.InvalidDateTime, + } + + return initUTC(dt.year, dt.month, dt.day, tm.hour, tm.min, tm.sec, tm.micros); + } + + pub fn add(dt: DateTime, value: i64, unit: TimeUnit) !DateTime { + const micros = dt.micros; + switch (unit) { + .days => return fromUnix(micros + value * MICROSECONDS_IN_A_DAY, .microseconds), + .hours => return fromUnix(micros + value * MICROSECONDS_IN_AN_HOUR, .microseconds), + .minutes => return fromUnix(micros + value * MICROSECONDS_IN_A_MIN, .microseconds), + .seconds => return fromUnix(micros + value * MICROSECONDS_IN_A_SEC, .microseconds), + .milliseconds => return fromUnix(micros + value * 1_000, .microseconds), + .microseconds => return fromUnix(micros + value, .microseconds), + } + } + + pub fn sub(a: DateTime, b: DateTime, precision: TimestampPrecision) i64 { + return a.unix(precision) - b.unix(precision); + } + + // https://git.musl-libc.org/cgit/musl/tree/src/time/__secs_to_tm.c?h=v0.9.15 + pub fn date(dt: DateTime) Date { + // 2000-03-01 (mod 400 year, immediately after feb29 + const leap_epoch = 946684800 + 86400 * (31 + 29); + const days_per_400y = 365 * 400 + 97; + const days_per_100y = 365 * 100 + 24; + const days_per_4y = 365 * 4 + 1; + + // march-based + const month_days = [_]u8{ 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29 }; + + const secs = @divTrunc(dt.micros, 1_000_000) - leap_epoch; + + var days = @divTrunc(secs, 86400); + if (@rem(secs, 86400) < 0) { + days -= 1; + } + + var qc_cycles = @divTrunc(days, days_per_400y); + var rem_days = @rem(days, days_per_400y); + if (rem_days < 0) { + rem_days += days_per_400y; + qc_cycles -= 1; + } + + var c_cycles = @divTrunc(rem_days, days_per_100y); + if (c_cycles == 4) { + c_cycles -= 1; + } + rem_days -= c_cycles * days_per_100y; + + var q_cycles = @divTrunc(rem_days, days_per_4y); + if (q_cycles == 25) { + q_cycles -= 1; + } + rem_days -= q_cycles * days_per_4y; + + var rem_years = @divTrunc(rem_days, 365); + if (rem_years == 4) { + rem_years -= 1; + } + rem_days -= rem_years * 365; + + var year = rem_years + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles + 2000; + + var month: u8 = 0; + while (month_days[month] <= rem_days) : (month += 1) { + rem_days -= month_days[month]; + } + + month += 2; + if (month >= 12) { + year += 1; + month -= 12; + } + + return .{ + .year = @intCast(year), + .month = month + 1, + .day = @intCast(rem_days + 1), + }; + } + + pub fn time(dt: DateTime) Time { + const micros = @mod(dt.micros, MICROSECONDS_IN_A_DAY); + + return .{ + .hour = @intCast(@divTrunc(micros, MICROSECONDS_IN_AN_HOUR)), + .min = @intCast(@divTrunc(@rem(micros, MICROSECONDS_IN_AN_HOUR), MICROSECONDS_IN_A_MIN)), + .sec = @intCast(@divTrunc(@rem(micros, MICROSECONDS_IN_A_MIN), MICROSECONDS_IN_A_SEC)), + .micros = @intCast(@rem(micros, MICROSECONDS_IN_A_SEC)), + }; + } + + pub fn unix(self: DateTime, precision: TimestampPrecision) i64 { + const micros = self.micros; + return switch (precision) { + .seconds => @divTrunc(micros, 1_000_000), + .milliseconds => @divTrunc(micros, 1_000), + .microseconds => micros, + }; + } + + pub fn order(a: DateTime, b: DateTime) std.math.Order { + return std.math.order(a.micros, b.micros); + } + + pub fn format(self: DateTime, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void { + var buf: [28]u8 = undefined; + const n = self.bufWrite(&buf); + try out.writeAll(buf[0..n]); + } + + pub fn jsonStringify(self: DateTime, out: anytype) !void { + var buf: [30]u8 = undefined; + buf[0] = '"'; + const n = self.bufWrite(buf[1..]); + buf[n + 1] = '"'; + try out.print("{s}", .{buf[0 .. n + 2]}); + } + + pub fn jsonParse(allocator: Allocator, source: anytype, options: anytype) !DateTime { + _ = options; + + switch (try source.nextAlloc(allocator, .alloc_if_needed)) { + inline .string, .allocated_string => |str| return parseRFC3339(str) catch return error.InvalidCharacter, + else => return error.UnexpectedToken, + } + } + + fn bufWrite(self: DateTime, buf: []u8) usize { + const date_n = writeDate(buf, self.date()); + + buf[date_n] = 'T'; + + const time_start = date_n + 1; + const time_n = writeTime(buf[time_start..], self.time()); + + const time_stop = time_start + time_n; + buf[time_stop] = 'Z'; + + return time_stop + 1; + } +}; + +fn writeDate(into: []u8, date: Date) u8 { + var buf: []u8 = undefined; + // cast year to a u16 so it doesn't insert a sign + // we don't want the + sign, ever + // and we don't even want it to insert the - sign, because it screws up + // the padding (we need to do it ourselfs) + const year = date.year; + if (year < 0) { + _ = std.fmt.formatIntBuf(into[1..], @as(u16, @intCast(year * -1)), 10, .lower, .{ .width = 4, .fill = '0' }); + into[0] = '-'; + buf = into[5..]; + } else { + _ = std.fmt.formatIntBuf(into, @as(u16, @intCast(year)), 10, .lower, .{ .width = 4, .fill = '0' }); + buf = into[4..]; + } + + buf[0] = '-'; + buf[1..3].* = paddingTwoDigits(date.month); + buf[3] = '-'; + buf[4..6].* = paddingTwoDigits(date.day); + + // return the length of the string. 10 for positive year, 11 for negative + return if (year < 0) 11 else 10; +} + +fn writeTime(into: []u8, time: Time) u8 { + into[0..2].* = paddingTwoDigits(time.hour); + into[2] = ':'; + into[3..5].* = paddingTwoDigits(time.min); + into[5] = ':'; + into[6..8].* = paddingTwoDigits(time.sec); + + const micros = time.micros; + if (micros == 0) { + return 8; + } + + if (@rem(micros, 1000) == 0) { + into[8] = '.'; + _ = std.fmt.formatIntBuf(into[9..12], micros / 1000, 10, .lower, .{ .width = 3, .fill = '0' }); + return 12; + } + + into[8] = '.'; + _ = std.fmt.formatIntBuf(into[9..15], micros, 10, .lower, .{ .width = 6, .fill = '0' }); + return 15; +} + +fn paddingTwoDigits(value: usize) [2]u8 { + std.debug.assert(value < 61); + const digits = "0001020304050607080910111213141516171819" ++ + "2021222324252627282930313233343536373839" ++ + "4041424344454647484950515253545556575859" ++ + "60"; + return digits[value * 2 ..][0..2].*; +} + +const Parser = struct { + input: []const u8, + pos: usize, + + fn init(input: []const u8) Parser { + return .{ + .pos = 0, + .input = input, + }; + } + + fn unconsumed(self: *const Parser) usize { + return self.input.len - self.pos; + } + + fn rest(self: *const Parser) []const u8 { + return self.input[self.pos..]; + } + + // unsafe, assumes caller has checked remaining first + fn peek(self: *const Parser) u8 { + return self.input[self.pos]; + } + + // unsafe, assumes caller has checked remaining first + fn consumeIf(self: *Parser, c: u8) bool { + const pos = self.pos; + if (self.input[pos] != c) { + return false; + } + self.pos = pos + 1; + return true; + } + + fn consumeN(self: *Parser, n: usize) ?[]const u8 { + const pos = self.pos; + const end = pos + n; + if (end > self.input.len) { + return null; + } + + defer self.pos = end; + return self.input[pos..end]; + } + + fn nanoseconds(self: *Parser) ?usize { + const start = self.pos; + const input = self.input[start..]; + + var len = input.len; + if (len == 0) { + return null; + } + + var value: usize = 0; + for (input, 0..) |b, i| { + const n = b -% '0'; // wrapping subtraction + if (n > 9) { + len = i; + break; + } + value = value * 10 + n; + } + + if (len > 9) { + return null; + } + + self.pos = start + len; + return value * std.math.pow(usize, 10, 9 - len); + } + + fn paddedInt(self: *Parser, comptime T: type, size: u8) ?T { + const pos = self.pos; + const end = pos + size; + const input = self.input; + + if (end > input.len) { + return null; + } + + var value: T = 0; + for (input[pos..end]) |b| { + const n = b -% '0'; // wrapping subtraction + if (n > 9) return null; + value = value * 10 + n; + } + self.pos = end; + return value; + } + + fn time(self: *Parser, allow_nano: bool) !Time { + const len = self.unconsumed(); + if (len < 5) { + return error.InvalidTime; + } + + const hour = self.paddedInt(u8, 2) orelse return error.InvalidTime; + if (self.consumeIf(':') == false) { + return error.InvalidTime; + } + + const min = self.paddedInt(u8, 2) orelse return error.InvalidTime; + if (len == 5 or self.consumeIf(':') == false) { + return Time.init(hour, min, 0, 0); + } + + const sec = self.paddedInt(u8, 2) orelse return error.InvalidTime; + if (allow_nano == false or len == 8 or self.consumeIf('.') == false) { + return Time.init(hour, min, sec, 0); + } + + const nanos = self.nanoseconds() orelse return error.InvalidTime; + return Time.init(hour, min, sec, @intCast(nanos / 1000)); + } + + fn iso8601Date(self: *Parser) !Date { + const len = self.unconsumed(); + if (len < 8) { + return error.InvalidDate; + } + + const negative = self.consumeIf('-'); + const year = self.paddedInt(i16, 4) orelse return error.InvalidDate; + + var with_dashes = false; + if (self.consumeIf('-')) { + if (len < 10) { + return error.InvalidDate; + } + with_dashes = true; + } + + const month = self.paddedInt(u8, 2) orelse return error.InvalidDate; + if (self.consumeIf('-') == !with_dashes) { + return error.InvalidDate; + } + + const day = self.paddedInt(u8, 2) orelse return error.InvalidDate; + return Date.init(if (negative) -year else year, month, day); + } + + fn rfc3339Date(self: *Parser) !Date { + const len = self.unconsumed(); + if (len < 10) { + return error.InvalidDate; + } + + const negative = self.consumeIf('-'); + const year = self.paddedInt(i16, 4) orelse return error.InvalidDate; + + if (self.consumeIf('-') == false) { + return error.InvalidDate; + } + + const month = self.paddedInt(u8, 2) orelse return error.InvalidDate; + + if (self.consumeIf('-') == false) { + return error.InvalidDate; + } + + const day = self.paddedInt(u8, 2) orelse return error.InvalidDate; + return Date.init(if (negative) -year else year, month, day); + } +}; + +const testing = @import("testing.zig"); +test "Date: json" { + { + // date, positive year + const date = Date{ .year = 2023, .month = 9, .day = 22 }; + const out = try std.json.stringifyAlloc(testing.allocator, date, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"2023-09-22\"", out); + } + + { + // date, negative year + const date = Date{ .year = -4, .month = 12, .day = 3 }; + const out = try std.json.stringifyAlloc(testing.allocator, date, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"-0004-12-03\"", out); + } + + { + // parse + const ts = try std.json.parseFromSlice(TestStruct, testing.allocator, "{\"date\":\"2023-09-22\"}", .{}); + defer ts.deinit(); + try testing.expectEqual(Date{ .year = 2023, .month = 9, .day = 22 }, ts.value.date.?); + } +} + +test "Date: format" { + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Date{ .year = 2023, .month = 5, .day = 22 }}); + try testing.expectString("2023-05-22", out); + } + + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Date{ .year = -102, .month = 12, .day = 9 }}); + try testing.expectString("-0102-12-09", out); + } +} + +test "Date: parse ISO8601" { + { + //valid YYYY-MM-DD + try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 22 }, try Date.parse("2023-05-22", .iso8601)); + try testing.expectEqual(Date{ .year = -2023, .month = 2, .day = 3 }, try Date.parse("-2023-02-03", .iso8601)); + try testing.expectEqual(Date{ .year = 1, .month = 2, .day = 3 }, try Date.parse("0001-02-03", .iso8601)); + try testing.expectEqual(Date{ .year = -1, .month = 2, .day = 3 }, try Date.parse("-0001-02-03", .iso8601)); + } + + { + //valid YYYYMMDD + try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 22 }, try Date.parse("20230522", .iso8601)); + try testing.expectEqual(Date{ .year = -2023, .month = 2, .day = 3 }, try Date.parse("-20230203", .iso8601)); + try testing.expectEqual(Date{ .year = 1, .month = 2, .day = 3 }, try Date.parse("00010203", .iso8601)); + try testing.expectEqual(Date{ .year = -1, .month = 2, .day = 3 }, try Date.parse("-00010203", .iso8601)); + } +} + +test "Date: parse RFC339" { + { + //valid YYYY-MM-DD + try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 22 }, try Date.parse("2023-05-22", .rfc3339)); + try testing.expectEqual(Date{ .year = -2023, .month = 2, .day = 3 }, try Date.parse("-2023-02-03", .rfc3339)); + try testing.expectEqual(Date{ .year = 1, .month = 2, .day = 3 }, try Date.parse("0001-02-03", .rfc3339)); + try testing.expectEqual(Date{ .year = -1, .month = 2, .day = 3 }, try Date.parse("-0001-02-03", .rfc3339)); + } + + { + //valid YYYYMMDD + try testing.expectError(error.InvalidDate, Date.parse("20230522", .rfc3339)); + try testing.expectError(error.InvalidDate, Date.parse("-20230203", .rfc3339)); + try testing.expectError(error.InvalidDate, Date.parse("00010203", .rfc3339)); + try testing.expectError(error.InvalidDate, Date.parse("-00010203", .rfc3339)); + } +} + +test "Date: parse invalid common" { + for (&[_]Date.Format{ .rfc3339, .iso8601 }) |format| { + { + // invalid format + try testing.expectError(error.InvalidDate, Date.parse("", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023/01-02", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-01/02", format)); + try testing.expectError(error.InvalidDate, Date.parse("0001-01-01 ", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-1-02", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-01-2", format)); + try testing.expectError(error.InvalidDate, Date.parse("9-01-2", format)); + try testing.expectError(error.InvalidDate, Date.parse("99-01-2", format)); + try testing.expectError(error.InvalidDate, Date.parse("999-01-2", format)); + try testing.expectError(error.InvalidDate, Date.parse("-999-01-2", format)); + try testing.expectError(error.InvalidDate, Date.parse("-1-01-2", format)); + } + + { + // invalid month + try testing.expectError(error.InvalidDate, Date.parse("2023-00-22", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-0A-22", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-13-22", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-99-22", format)); + try testing.expectError(error.InvalidDate, Date.parse("-2023-00-22", format)); + try testing.expectError(error.InvalidDate, Date.parse("-2023-13-22", format)); + try testing.expectError(error.InvalidDate, Date.parse("-2023-99-22", format)); + } + + { + // invalid day + try testing.expectError(error.InvalidDate, Date.parse("2023-01-00", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-01-32", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-02-29", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-03-32", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-04-31", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-05-32", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-06-31", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-07-32", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-08-32", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-09-31", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-10-32", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-11-31", format)); + try testing.expectError(error.InvalidDate, Date.parse("2023-12-32", format)); + } + + { + // valid (max day) + try testing.expectEqual(Date{ .year = 2023, .month = 1, .day = 31 }, try Date.parse("2023-01-31", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 2, .day = 28 }, try Date.parse("2023-02-28", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 3, .day = 31 }, try Date.parse("2023-03-31", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 4, .day = 30 }, try Date.parse("2023-04-30", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 5, .day = 31 }, try Date.parse("2023-05-31", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 6, .day = 30 }, try Date.parse("2023-06-30", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 7, .day = 31 }, try Date.parse("2023-07-31", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 8, .day = 31 }, try Date.parse("2023-08-31", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 9, .day = 30 }, try Date.parse("2023-09-30", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 10, .day = 31 }, try Date.parse("2023-10-31", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 30 }, try Date.parse("2023-11-30", format)); + try testing.expectEqual(Date{ .year = 2023, .month = 12, .day = 31 }, try Date.parse("2023-12-31", format)); + } + + { + // leap years + try testing.expectEqual(Date{ .year = 2000, .month = 2, .day = 29 }, try Date.parse("2000-02-29", format)); + try testing.expectEqual(Date{ .year = 2400, .month = 2, .day = 29 }, try Date.parse("2400-02-29", format)); + try testing.expectEqual(Date{ .year = 2012, .month = 2, .day = 29 }, try Date.parse("2012-02-29", format)); + try testing.expectEqual(Date{ .year = 2024, .month = 2, .day = 29 }, try Date.parse("2024-02-29", format)); + + try testing.expectError(error.InvalidDate, Date.parse("2000-02-30", format)); + try testing.expectError(error.InvalidDate, Date.parse("2400-02-30", format)); + try testing.expectError(error.InvalidDate, Date.parse("2012-02-30", format)); + try testing.expectError(error.InvalidDate, Date.parse("2024-02-30", format)); + + try testing.expectError(error.InvalidDate, Date.parse("2100-02-29", format)); + try testing.expectError(error.InvalidDate, Date.parse("2200-02-29", format)); + } + } +} + +test "Date: order" { + { + const a = Date{ .year = 2023, .month = 5, .day = 22 }; + const b = Date{ .year = 2023, .month = 5, .day = 22 }; + try testing.expectEqual(std.math.Order.eq, a.order(b)); + } + + { + const a = Date{ .year = 2023, .month = 5, .day = 22 }; + const b = Date{ .year = 2022, .month = 5, .day = 22 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = Date{ .year = 2022, .month = 6, .day = 22 }; + const b = Date{ .year = 2022, .month = 5, .day = 22 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = Date{ .year = 2023, .month = 5, .day = 23 }; + const b = Date{ .year = 2022, .month = 5, .day = 22 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } +} + +test "Time: json" { + { + // time no fraction + const time = Time{ .hour = 23, .min = 59, .sec = 2, .micros = 0 }; + const out = try std.json.stringifyAlloc(testing.allocator, time, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"23:59:02\"", out); + } + + { + // time, milliseconds only + const time = Time{ .hour = 7, .min = 9, .sec = 32, .micros = 202000 }; + const out = try std.json.stringifyAlloc(testing.allocator, time, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"07:09:32.202\"", out); + } + + { + // time, micros + const time = Time{ .hour = 1, .min = 2, .sec = 3, .micros = 123456 }; + const out = try std.json.stringifyAlloc(testing.allocator, time, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"01:02:03.123456\"", out); + } + + { + // parse + const ts = try std.json.parseFromSlice(TestStruct, testing.allocator, "{\"time\":\"01:02:03.123456\"}", .{}); + defer ts.deinit(); + try testing.expectEqual(Time{ .hour = 1, .min = 2, .sec = 3, .micros = 123456 }, ts.value.time.?); + } +} + +test "Time: format" { + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Time{ .hour = 23, .min = 59, .sec = 59, .micros = 0 }}); + try testing.expectString("23:59:59", out); + } + + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 12 }}); + try testing.expectString("08:09:10.000012", out); + } + + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 123 }}); + try testing.expectString("08:09:10.000123", out); + } + + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 1234 }}); + try testing.expectString("08:09:10.001234", out); + } + + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 12345 }}); + try testing.expectString("08:09:10.012345", out); + } + + { + var buf: [20]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{Time{ .hour = 8, .min = 9, .sec = 10, .micros = 123456 }}); + try testing.expectString("08:09:10.123456", out); + } +} + +test "Time: parse" { + { + //valid + try testing.expectEqual(Time{ .hour = 9, .min = 8, .sec = 0, .micros = 0 }, try Time.parse("09:08", .rfc3339)); + try testing.expectEqual(Time{ .hour = 9, .min = 8, .sec = 5, .micros = 123000 }, try Time.parse("09:08:05.123", .rfc3339)); + try testing.expectEqual(Time{ .hour = 23, .min = 59, .sec = 59, .micros = 0 }, try Time.parse("23:59:59", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 0 }, try Time.parse("00:00:00", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 0 }, try Time.parse("00:00:00.0", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 1 }, try Time.parse("00:00:00.000001", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 12 }, try Time.parse("00:00:00.000012", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123 }, try Time.parse("00:00:00.000123", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 1234 }, try Time.parse("00:00:00.001234", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 12345 }, try Time.parse("00:00:00.012345", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse("00:00:00.123456", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse("00:00:00.1234567", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse("00:00:00.12345678", .rfc3339)); + try testing.expectEqual(Time{ .hour = 0, .min = 0, .sec = 0, .micros = 123456 }, try Time.parse("00:00:00.123456789", .rfc3339)); + } + + { + try testing.expectError(error.InvalidTime, Time.parse("", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("01:00:", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("1:00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("10:1:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("10:11:4", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("10:20:30.", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("10:20:30.a", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("10:20:30.1234567899", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("10:20:30.123Z", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("24:00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("00:60:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("00:00:60", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("0a:00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("00:0a:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("00:00:0a", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("00/00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, Time.parse("00:00 00", .rfc3339)); + } +} + +test "Time: order" { + { + const a = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 }; + const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 }; + try testing.expectEqual(std.math.Order.eq, a.order(b)); + } + + { + const a = Time{ .hour = 20, .min = 17, .sec = 22, .micros = 101002 }; + const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = Time{ .hour = 19, .min = 18, .sec = 22, .micros = 101002 }; + const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = Time{ .hour = 19, .min = 17, .sec = 23, .micros = 101002 }; + const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101003 }; + const b = Time{ .hour = 19, .min = 17, .sec = 22, .micros = 101002 }; + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } +} + +test "DateTime: initUTC" { + // GO + // for i := 0; i < 100; i++ { + // us := rand.Int63n(31536000000000000) + // if i%2 == 1 { + // us = -us + // } + // date := time.UnixMicro(us).UTC() + // fmt.Printf("\ttry testing.expectEqual(%d, (try DateTime.initUTC(%d, %d, %d, %d, %d, %d, %d)).micros);\n", us, date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), date.Nanosecond()/1000) + // } + try testing.expectEqual(31185488490276150, (try DateTime.initUTC(2958, 3, 25, 3, 41, 30, 276150)).micros); + try testing.expectEqual(-17564653328342207, (try DateTime.initUTC(1413, 5, 26, 9, 37, 51, 657793)).micros); + try testing.expectEqual(11204762425459393, (try DateTime.initUTC(2325, 1, 24, 18, 0, 25, 459393)).micros); + try testing.expectEqual(-11416605162739875, (try DateTime.initUTC(1608, 3, 22, 8, 47, 17, 260125)).micros); + try testing.expectEqual(4075732367920414, (try DateTime.initUTC(2099, 2, 25, 19, 52, 47, 920414)).micros); + try testing.expectEqual(-18408335598163579, (try DateTime.initUTC(1386, 8, 30, 13, 26, 41, 836421)).micros); + try testing.expectEqual(17086490946271926, (try DateTime.initUTC(2511, 6, 14, 7, 29, 6, 271926)).micros); + try testing.expectEqual(-235277150936616, (try DateTime.initUTC(1962, 7, 18, 21, 14, 9, 63384)).micros); + try testing.expectEqual(11104788804726682, (try DateTime.initUTC(2321, 11, 24, 15, 33, 24, 726682)).micros); + try testing.expectEqual(-4568937205156452, (try DateTime.initUTC(1825, 3, 20, 18, 46, 34, 843548)).micros); + try testing.expectEqual(24765673968274275, (try DateTime.initUTC(2754, 10, 17, 17, 52, 48, 274275)).micros); + try testing.expectEqual(-7121990846251510, (try DateTime.initUTC(1744, 4, 24, 13, 12, 33, 748490)).micros); + try testing.expectEqual(17226397205968456, (try DateTime.initUTC(2515, 11, 19, 14, 20, 5, 968456)).micros); + try testing.expectEqual(-6754262392339050, (try DateTime.initUTC(1755, 12, 19, 16, 0, 7, 660950)).micros); + try testing.expectEqual(16357572620714009, (try DateTime.initUTC(2488, 5, 7, 18, 10, 20, 714009)).micros); + try testing.expectEqual(-25688820176639049, (try DateTime.initUTC(1155, 12, 15, 16, 37, 3, 360951)).micros); + try testing.expectEqual(20334458172336139, (try DateTime.initUTC(2614, 5, 17, 12, 36, 12, 336139)).micros); + try testing.expectEqual(-30602962159178117, (try DateTime.initUTC(1000, 3, 26, 1, 10, 40, 821883)).micros); + try testing.expectEqual(10851036879825648, (try DateTime.initUTC(2313, 11, 9, 16, 54, 39, 825648)).micros); + try testing.expectEqual(-21853769826060317, (try DateTime.initUTC(1277, 6, 24, 20, 22, 53, 939683)).micros); + try testing.expectEqual(23747326217087461, (try DateTime.initUTC(2722, 7, 11, 7, 30, 17, 87461)).micros); + try testing.expectEqual(-6579703114708064, (try DateTime.initUTC(1761, 7, 1, 0, 41, 25, 291936)).micros); + try testing.expectEqual(14734931422924073, (try DateTime.initUTC(2436, 12, 6, 4, 30, 22, 924073)).micros); + try testing.expectEqual(-14370161672281011, (try DateTime.initUTC(1514, 8, 18, 16, 25, 27, 718989)).micros); + try testing.expectEqual(21611484560584058, (try DateTime.initUTC(2654, 11, 3, 22, 9, 20, 584058)).micros); + try testing.expectEqual(-15774514890527755, (try DateTime.initUTC(1470, 2, 15, 14, 18, 29, 472245)).micros); + try testing.expectEqual(12457884381373706, (try DateTime.initUTC(2364, 10, 10, 11, 26, 21, 373706)).micros); + try testing.expectEqual(-9291409512875127, (try DateTime.initUTC(1675, 7, 26, 12, 54, 47, 124873)).micros); + try testing.expectEqual(18766703512694310, (try DateTime.initUTC(2564, 9, 10, 5, 11, 52, 694310)).micros); + try testing.expectEqual(-10898338457124469, (try DateTime.initUTC(1624, 8, 23, 19, 45, 42, 875531)).micros); + try testing.expectEqual(27404278841361952, (try DateTime.initUTC(2838, 5, 29, 3, 40, 41, 361952)).micros); + try testing.expectEqual(-11493696741549109, (try DateTime.initUTC(1605, 10, 12, 2, 27, 38, 450891)).micros); + try testing.expectEqual(25167839321247044, (try DateTime.initUTC(2767, 7, 16, 10, 28, 41, 247044)).micros); + try testing.expectEqual(-8645720427930599, (try DateTime.initUTC(1696, 1, 10, 18, 59, 32, 69401)).micros); + try testing.expectEqual(7021225980669527, (try DateTime.initUTC(2192, 6, 29, 4, 33, 0, 669527)).micros); + try testing.expectEqual(-22567421500525473, (try DateTime.initUTC(1254, 11, 12, 23, 48, 19, 474527)).micros); + try testing.expectEqual(3592419409525180, (try DateTime.initUTC(2083, 11, 2, 22, 16, 49, 525180)).micros); + try testing.expectEqual(-24897829995733878, (try DateTime.initUTC(1181, 1, 7, 16, 6, 44, 266122)).micros); + try testing.expectEqual(1801796752202729, (try DateTime.initUTC(2027, 2, 5, 3, 5, 52, 202729)).micros); + try testing.expectEqual(-21458729756349585, (try DateTime.initUTC(1289, 12, 31, 1, 44, 3, 650415)).micros); + try testing.expectEqual(27431277767015263, (try DateTime.initUTC(2839, 4, 6, 15, 22, 47, 15263)).micros); + try testing.expectEqual(-11932647633976328, (try DateTime.initUTC(1591, 11, 14, 15, 39, 26, 23672)).micros); + try testing.expectEqual(11561116817530249, (try DateTime.initUTC(2336, 5, 11, 5, 20, 17, 530249)).micros); + try testing.expectEqual(-20238374988448844, (try DateTime.initUTC(1328, 9, 2, 13, 10, 11, 551156)).micros); + try testing.expectEqual(17825448287939368, (try DateTime.initUTC(2534, 11, 13, 1, 24, 47, 939368)).micros); + try testing.expectEqual(-16551182110752962, (try DateTime.initUTC(1445, 7, 7, 9, 24, 49, 247038)).micros); + try testing.expectEqual(7773488831126355, (try DateTime.initUTC(2216, 5, 1, 22, 27, 11, 126355)).micros); + try testing.expectEqual(-17967725644400042, (try DateTime.initUTC(1400, 8, 17, 5, 5, 55, 599958)).micros); + try testing.expectEqual(30634276344447791, (try DateTime.initUTC(2940, 10, 5, 9, 12, 24, 447791)).micros); + try testing.expectEqual(-3201531339091604, (try DateTime.initUTC(1868, 7, 19, 5, 44, 20, 908396)).micros); + try testing.expectEqual(16621702451341054, (try DateTime.initUTC(2496, 9, 19, 19, 34, 11, 341054)).micros); + try testing.expectEqual(-12321145808433043, (try DateTime.initUTC(1579, 7, 24, 3, 29, 51, 566957)).micros); + try testing.expectEqual(116851935152341, (try DateTime.initUTC(1973, 9, 14, 10, 52, 15, 152341)).micros); + try testing.expectEqual(-26516365395395707, (try DateTime.initUTC(1129, 9, 24, 14, 56, 44, 604293)).micros); + try testing.expectEqual(29944637164250909, (try DateTime.initUTC(2918, 11, 28, 10, 46, 4, 250909)).micros); + try testing.expectEqual(-14268089958574835, (try DateTime.initUTC(1517, 11, 12, 1, 40, 41, 425165)).micros); + try testing.expectEqual(10902808879115327, (try DateTime.initUTC(2315, 7, 1, 22, 1, 19, 115327)).micros); + try testing.expectEqual(-13675746347719473, (try DateTime.initUTC(1536, 8, 19, 21, 34, 12, 280527)).micros); + try testing.expectEqual(9823904882276154, (try DateTime.initUTC(2281, 4, 22, 14, 28, 2, 276154)).micros); + try testing.expectEqual(-8027825490751946, (try DateTime.initUTC(1715, 8, 11, 8, 28, 29, 248054)).micros); + try testing.expectEqual(8338818189787922, (try DateTime.initUTC(2234, 4, 1, 2, 23, 9, 787922)).micros); + try testing.expectEqual(-2417779710874201, (try DateTime.initUTC(1893, 5, 20, 10, 31, 29, 125799)).micros); + try testing.expectEqual(15579463520321126, (try DateTime.initUTC(2463, 9, 10, 20, 45, 20, 321126)).micros); + try testing.expectEqual(-30111774746323219, (try DateTime.initUTC(1015, 10, 19, 2, 7, 33, 676781)).micros); + try testing.expectEqual(8586318907201828, (try DateTime.initUTC(2242, 2, 2, 16, 35, 7, 201828)).micros); + try testing.expectEqual(-20727462914538728, (try DateTime.initUTC(1313, 3, 4, 19, 24, 45, 461272)).micros); + try testing.expectEqual(12684924982677857, (try DateTime.initUTC(2371, 12, 21, 6, 16, 22, 677857)).micros); + try testing.expectEqual(-26995363453933698, (try DateTime.initUTC(1114, 7, 21, 15, 55, 46, 66302)).micros); + try testing.expectEqual(5769549719315448, (try DateTime.initUTC(2152, 10, 30, 4, 41, 59, 315448)).micros); + try testing.expectEqual(-9362762735064704, (try DateTime.initUTC(1673, 4, 21, 16, 34, 24, 935296)).micros); + try testing.expectEqual(5196087673076825, (try DateTime.initUTC(2134, 8, 28, 21, 41, 13, 76825)).micros); + try testing.expectEqual(-10198286600499296, (try DateTime.initUTC(1646, 10, 30, 6, 36, 39, 500704)).micros); + try testing.expectEqual(19333137979539125, (try DateTime.initUTC(2582, 8, 23, 4, 6, 19, 539125)).micros); + try testing.expectEqual(-18867539824804327, (try DateTime.initUTC(1372, 2, 10, 16, 42, 55, 195673)).micros); + try testing.expectEqual(14853031249581056, (try DateTime.initUTC(2440, 9, 3, 2, 0, 49, 581056)).micros); + try testing.expectEqual(-1356282109230506, (try DateTime.initUTC(1927, 1, 9, 6, 58, 10, 769494)).micros); + try testing.expectEqual(15713222018105813, (try DateTime.initUTC(2467, 12, 6, 23, 53, 38, 105813)).micros); + try testing.expectEqual(-12693041975378709, (try DateTime.initUTC(1567, 10, 10, 19, 0, 24, 621291)).micros); + try testing.expectEqual(29394313298789588, (try DateTime.initUTC(2901, 6, 20, 23, 1, 38, 789588)).micros); + try testing.expectEqual(-10583952098364782, (try DateTime.initUTC(1634, 8, 10, 13, 18, 21, 635218)).micros); + try testing.expectEqual(22418800474726154, (try DateTime.initUTC(2680, 6, 3, 20, 34, 34, 726154)).micros); + try testing.expectEqual(-13067278028607441, (try DateTime.initUTC(1555, 12, 1, 8, 32, 51, 392559)).micros); + try testing.expectEqual(22348003126725817, (try DateTime.initUTC(2678, 3, 7, 10, 38, 46, 725817)).micros); + try testing.expectEqual(-11101998054915852, (try DateTime.initUTC(1618, 3, 11, 15, 39, 5, 84148)).micros); + try testing.expectEqual(30004645932503986, (try DateTime.initUTC(2920, 10, 22, 23, 52, 12, 503986)).micros); + try testing.expectEqual(-27551013013624622, (try DateTime.initUTC(1096, 12, 10, 12, 49, 46, 375378)).micros); + try testing.expectEqual(10162791607756167, (try DateTime.initUTC(2292, 1, 17, 21, 40, 7, 756167)).micros); + try testing.expectEqual(-31309636417799549, (try DateTime.initUTC(977, 11, 1, 22, 46, 22, 200451)).micros); + try testing.expectEqual(9816298180956872, (try DateTime.initUTC(2281, 1, 24, 13, 29, 40, 956872)).micros); + try testing.expectEqual(-13248552913008079, (try DateTime.initUTC(1550, 3, 4, 6, 24, 46, 991921)).micros); + try testing.expectEqual(24898184818866845, (try DateTime.initUTC(2758, 12, 29, 10, 26, 58, 866845)).micros); + try testing.expectEqual(-10721424878768860, (try DateTime.initUTC(1630, 4, 2, 10, 25, 21, 231140)).micros); + try testing.expectEqual(3556757075942051, (try DateTime.initUTC(2082, 9, 16, 4, 4, 35, 942051)).micros); + try testing.expectEqual(-9515936853544912, (try DateTime.initUTC(1668, 6, 13, 20, 12, 26, 455088)).micros); + try testing.expectEqual(23236928933459964, (try DateTime.initUTC(2706, 5, 8, 22, 28, 53, 459964)).micros); + try testing.expectEqual(-5811784886171477, (try DateTime.initUTC(1785, 10, 30, 23, 18, 33, 828523)).micros); + try testing.expectEqual(27342496921109542, (try DateTime.initUTC(2836, 6, 13, 2, 2, 1, 109542)).micros); + try testing.expectEqual(-25369943235288340, (try DateTime.initUTC(1166, 1, 22, 9, 32, 44, 711660)).micros); + try testing.expectEqual(10054378230055484, (try DateTime.initUTC(2288, 8, 11, 2, 50, 30, 55484)).micros); + try testing.expectEqual(-10826899878642792, (try DateTime.initUTC(1626, 11, 28, 15, 48, 41, 357208)).micros); +} + +test "DateTime: now" { + const dt = DateTime.now(); + try testing.expectDelta(std.time.microTimestamp(), dt.micros, 1000); +} + +test "DateTime: date" { + try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 25 }, (try DateTime.fromUnix(1700886257, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 25 }, (try DateTime.fromUnix(1700886257655, .milliseconds)).date()); + try testing.expectEqual(Date{ .year = 2023, .month = 11, .day = 25 }, (try DateTime.fromUnix(1700886257655392, .microseconds)).date()); + try testing.expectEqual(Date{ .year = 1970, .month = 1, .day = 1 }, (try DateTime.fromUnix(0, .milliseconds)).date()); + + // GO: + // for i := 0; i < 100; i++ { + // us := rand.Int63n(31536000000000000) + // if i%2 == 1 { + // us = -us + // } + // date := time.UnixMicro(us).UTC() + // fmt.Printf("\ttry testing.expectEqual(Date{.year = %d, .month = %d, .day = %d}, DateTime.fromUnix(%d, .seconds).date());\n", date.Year(), date.Month(), date.Day(), date.Unix()) + // } + try testing.expectEqual(Date{ .year = 2438, .month = 8, .day = 8 }, (try DateTime.fromUnix(14787635606, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1290, .month = 10, .day = 9 }, (try DateTime.fromUnix(-21434368940, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2769, .month = 12, .day = 3 }, (try DateTime.fromUnix(25243136028, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1437, .month = 6, .day = 30 }, (try DateTime.fromUnix(-16804239664, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2752, .month = 4, .day = 7 }, (try DateTime.fromUnix(24685876670, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1484, .month = 1, .day = 29 }, (try DateTime.fromUnix(-15334209737, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2300, .month = 1, .day = 4 }, (try DateTime.fromUnix(10414107497, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1520, .month = 3, .day = 27 }, (try DateTime.fromUnix(-14193188705, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2628, .month = 11, .day = 21 }, (try DateTime.fromUnix(20792540664, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1807, .month = 2, .day = 21 }, (try DateTime.fromUnix(-5139411928, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2249, .month = 12, .day = 12 }, (try DateTime.fromUnix(8834245007, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1694, .month = 11, .day = 17 }, (try DateTime.fromUnix(-8681990253, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2725, .month = 6, .day = 10 }, (try DateTime.fromUnix(23839369640, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1947, .month = 2, .day = 16 }, (try DateTime.fromUnix(-721811319, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2293, .month = 9, .day = 28 }, (try DateTime.fromUnix(10216323340, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1614, .month = 8, .day = 12 }, (try DateTime.fromUnix(-11214942944, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2923, .month = 6, .day = 24 }, (try DateTime.fromUnix(30088834422, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1120, .month = 4, .day = 16 }, (try DateTime.fromUnix(-26814276389, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2035, .month = 12, .day = 9 }, (try DateTime.fromUnix(2080850037, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1167, .month = 1, .day = 15 }, (try DateTime.fromUnix(-25338977309, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2665, .month = 4, .day = 15 }, (try DateTime.fromUnix(21941133655, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1375, .month = 6, .day = 18 }, (try DateTime.fromUnix(-18761787336, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2189, .month = 6, .day = 13 }, (try DateTime.fromUnix(6925211914, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1938, .month = 1, .day = 12 }, (try DateTime.fromUnix(-1008879186, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2556, .month = 6, .day = 9 }, (try DateTime.fromUnix(18506255391, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1294, .month = 10, .day = 29 }, (try DateTime.fromUnix(-21306371902, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2330, .month = 3, .day = 19 }, (try DateTime.fromUnix(11367189469, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1696, .month = 5, .day = 22 }, (try DateTime.fromUnix(-8634251099, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2759, .month = 5, .day = 14 }, (try DateTime.fromUnix(24909971092, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1641, .month = 1, .day = 31 }, (try DateTime.fromUnix(-10379518549, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2451, .month = 6, .day = 26 }, (try DateTime.fromUnix(15194147684, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1962, .month = 1, .day = 4 }, (try DateTime.fromUnix(-252197440, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2883, .month = 11, .day = 15 }, (try DateTime.fromUnix(28839089617, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1587, .month = 8, .day = 5 }, (try DateTime.fromUnix(-12067604792, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2724, .month = 5, .day = 28 }, (try DateTime.fromUnix(23806729201, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1043, .month = 2, .day = 25 }, (try DateTime.fromUnix(-29248487174, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2927, .month = 3, .day = 9 }, (try DateTime.fromUnix(30205844459, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1451, .month = 6, .day = 16 }, (try DateTime.fromUnix(-16363722083, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2145, .month = 1, .day = 21 }, (try DateTime.fromUnix(5524305523, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1497, .month = 10, .day = 31 }, (try DateTime.fromUnix(-14900125085, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2162, .month = 4, .day = 1 }, (try DateTime.fromUnix(6066812142, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1738, .month = 8, .day = 12 }, (try DateTime.fromUnix(-7301852750, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2100, .month = 2, .day = 7 }, (try DateTime.fromUnix(4105665807, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1847, .month = 9, .day = 29 }, (try DateTime.fromUnix(-3858020808, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2370, .month = 9, .day = 19 }, (try DateTime.fromUnix(12645416176, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1292, .month = 7, .day = 8 }, (try DateTime.fromUnix(-21379166225, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2931, .month = 12, .day = 19 }, (try DateTime.fromUnix(30356691249, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1064, .month = 5, .day = 12 }, (try DateTime.fromUnix(-28579189254, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2295, .month = 5, .day = 13 }, (try DateTime.fromUnix(10267494406, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1449, .month = 12, .day = 4 }, (try DateTime.fromUnix(-16411941423, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2565, .month = 1, .day = 16 }, (try DateTime.fromUnix(18777760055, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1968, .month = 6, .day = 25 }, (try DateTime.fromUnix(-47882241, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2817, .month = 5, .day = 9 }, (try DateTime.fromUnix(26739900891, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1334, .month = 7, .day = 16 }, (try DateTime.fromUnix(-20053254809, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2945, .month = 4, .day = 24 }, (try DateTime.fromUnix(30777844895, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1930, .month = 2, .day = 27 }, (try DateTime.fromUnix(-1257362995, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2768, .month = 10, .day = 19 }, (try DateTime.fromUnix(25207675701, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1372, .month = 6, .day = 12 }, (try DateTime.fromUnix(-18856904218, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2603, .month = 8, .day = 29 }, (try DateTime.fromUnix(19996315706, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1201, .month = 4, .day = 7 }, (try DateTime.fromUnix(-24258926407, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2466, .month = 4, .day = 16 }, (try DateTime.fromUnix(15661407305, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1513, .month = 5, .day = 7 }, (try DateTime.fromUnix(-14410616341, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2619, .month = 9, .day = 11 }, (try DateTime.fromUnix(20502308837, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1501, .month = 5, .day = 13 }, (try DateTime.fromUnix(-14788768973, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2765, .month = 11, .day = 19 }, (try DateTime.fromUnix(25115683551, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1881, .month = 2, .day = 9 }, (try DateTime.fromUnix(-2805094638, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2253, .month = 4, .day = 28 }, (try DateTime.fromUnix(8940802800, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1941, .month = 11, .day = 23 }, (try DateTime.fromUnix(-886973505, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2565, .month = 1, .day = 18 }, (try DateTime.fromUnix(18777963967, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1313, .month = 5, .day = 20 }, (try DateTime.fromUnix(-20720877804, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2401, .month = 5, .day = 6 }, (try DateTime.fromUnix(13611949193, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1146, .month = 11, .day = 2 }, (try DateTime.fromUnix(-25976564837, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2115, .month = 6, .day = 11 }, (try DateTime.fromUnix(4589719542, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1276, .month = 8, .day = 1 }, (try DateTime.fromUnix(-21882043432, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2224, .month = 4, .day = 26 }, (try DateTime.fromUnix(8025468043, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1336, .month = 6, .day = 19 }, (try DateTime.fromUnix(-19992405201, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2717, .month = 5, .day = 5 }, (try DateTime.fromUnix(23583761778, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1222, .month = 3, .day = 15 }, (try DateTime.fromUnix(-23598239244, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2841, .month = 8, .day = 29 }, (try DateTime.fromUnix(27506984246, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1818, .month = 7, .day = 28 }, (try DateTime.fromUnix(-4778656923, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2533, .month = 5, .day = 13 }, (try DateTime.fromUnix(17778031068, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1146, .month = 7, .day = 28 }, (try DateTime.fromUnix(-25984946441, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2451, .month = 2, .day = 1 }, (try DateTime.fromUnix(15181688532, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1091, .month = 8, .day = 28 }, (try DateTime.fromUnix(-27717880960, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2168, .month = 4, .day = 12 }, (try DateTime.fromUnix(6257133476, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1718, .month = 10, .day = 16 }, (try DateTime.fromUnix(-7927438165, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2614, .month = 8, .day = 21 }, (try DateTime.fromUnix(20342724001, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1869, .month = 5, .day = 4 }, (try DateTime.fromUnix(-3176499822, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2504, .month = 4, .day = 20 }, (try DateTime.fromUnix(16860953121, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1401, .month = 5, .day = 2 }, (try DateTime.fromUnix(-17945432544, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2467, .month = 8, .day = 2 }, (try DateTime.fromUnix(15702325347, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1654, .month = 3, .day = 12 }, (try DateTime.fromUnix(-9965864717, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2371, .month = 9, .day = 2 }, (try DateTime.fromUnix(12675412066, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1784, .month = 1, .day = 16 }, (try DateTime.fromUnix(-5868249970, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2907, .month = 8, .day = 25 }, (try DateTime.fromUnix(29589265328, .seconds)).date()); + try testing.expectEqual(Date{ .year = 987, .month = 4, .day = 9 }, (try DateTime.fromUnix(-31011963272, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1980, .month = 10, .day = 19 }, (try DateTime.fromUnix(340838803, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1386, .month = 5, .day = 18 }, (try DateTime.fromUnix(-18417299412, .seconds)).date()); + try testing.expectEqual(Date{ .year = 2622, .month = 2, .day = 5 }, (try DateTime.fromUnix(20578157994, .seconds)).date()); + try testing.expectEqual(Date{ .year = 1056, .month = 11, .day = 6 }, (try DateTime.fromUnix(-28816263601, .seconds)).date()); +} + +test "DateTime: time" { + // GO: + // for i := 0; i < 100; i++ { + // us := rand.Int63n(31536000000000000) + // if i%2 == 1 { + // us = -us + // } + // date := time.UnixMicro(us).UTC() + // fmt.Printf("\ttry testing.expectEqual(Time{.hour = %d, .min = %d, .sec = %d, .micros = %d}, (try DateTime.fromUnix(%d, .microseconds)).time());\n", date.Hour(), date.Minute(), date.Second(), date.Nanosecond()/1000, us) + // } + try testing.expectEqual(Time{ .hour = 18, .min = 56, .sec = 18, .micros = 38399 }, (try DateTime.fromUnix(6940752978038399, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 14, .min = 10, .sec = 48, .micros = 481799 }, (try DateTime.fromUnix(-15037004951518201, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 13, .min = 49, .sec = 27, .micros = 814723 }, (try DateTime.fromUnix(26507483367814723, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 3, .min = 53, .sec = 47, .micros = 990825 }, (try DateTime.fromUnix(-15290625972009175, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 9, .min = 28, .sec = 54, .micros = 16606 }, (try DateTime.fromUnix(28046078934016606, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 36, .sec = 38, .micros = 380600 }, (try DateTime.fromUnix(-8638640601619400, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 29, .sec = 27, .micros = 109527 }, (try DateTime.fromUnix(26649192567109527, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 23, .min = 54, .sec = 48, .micros = 10233 }, (try DateTime.fromUnix(-24667200311989767, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 44, .sec = 50, .micros = 913226 }, (try DateTime.fromUnix(22200932690913226, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 36, .sec = 19, .micros = 337687 }, (try DateTime.fromUnix(-13186952620662313, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 20, .min = 6, .sec = 37, .micros = 157270 }, (try DateTime.fromUnix(17827416397157270, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 4, .min = 43, .sec = 33, .micros = 871331 }, (try DateTime.fromUnix(-15558635786128669, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 0, .min = 26, .sec = 54, .micros = 557236 }, (try DateTime.fromUnix(23322644814557236, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 7, .min = 38, .sec = 40, .micros = 370732 }, (try DateTime.fromUnix(-1368030079629268, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 2, .min = 31, .sec = 9, .micros = 223691 }, (try DateTime.fromUnix(20164386669223691, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 12, .min = 41, .sec = 23, .micros = 165207 }, (try DateTime.fromUnix(-20761960716834793, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 0, .min = 46, .sec = 49, .micros = 962075 }, (try DateTime.fromUnix(549247609962075, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 2, .min = 7, .sec = 12, .micros = 984678 }, (try DateTime.fromUnix(-11643688367015322, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 11, .min = 32, .sec = 16, .micros = 343799 }, (try DateTime.fromUnix(4022998336343799, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 26, .sec = 54, .micros = 366277 }, (try DateTime.fromUnix(-8557597985633723, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 16, .min = 1, .sec = 4, .micros = 485152 }, (try DateTime.fromUnix(15070896064485152, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 4, .min = 14, .sec = 18, .micros = 923558 }, (try DateTime.fromUnix(-15995389541076442, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 37, .sec = 58, .micros = 948826 }, (try DateTime.fromUnix(16828148278948826, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 6, .min = 52, .sec = 27, .micros = 1770 }, (try DateTime.fromUnix(-30509975252998230, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 0, .min = 32, .sec = 28, .micros = 381047 }, (try DateTime.fromUnix(7813499548381047, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 14, .min = 1, .sec = 49, .micros = 267686 }, (try DateTime.fromUnix(-14265712690732314, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 4, .min = 53, .sec = 23, .micros = 233239 }, (try DateTime.fromUnix(31107646403233239, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 3, .min = 0, .sec = 53, .micros = 292242 }, (try DateTime.fromUnix(-10317099546707758, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 8, .min = 22, .sec = 13, .micros = 966628 }, (try DateTime.fromUnix(11215959733966628, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 32, .sec = 22, .micros = 779813 }, (try DateTime.fromUnix(-15711949657220187, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 1, .min = 6, .sec = 36, .micros = 405828 }, (try DateTime.fromUnix(6872691996405828, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 12, .min = 0, .sec = 55, .micros = 420129 }, (try DateTime.fromUnix(-31068273544579871, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 17, .sec = 6, .micros = 930158 }, (try DateTime.fromUnix(26304473826930158, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 12, .min = 45, .sec = 25, .micros = 203619 }, (try DateTime.fromUnix(-5358482074796381, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 19, .min = 28, .sec = 0, .micros = 476749 }, (try DateTime.fromUnix(9134623680476749, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 11, .min = 58, .sec = 41, .micros = 864572 }, (try DateTime.fromUnix(-29314353678135428, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 6, .min = 19, .sec = 27, .micros = 566937 }, (try DateTime.fromUnix(9005494767566937, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 9, .min = 3, .sec = 17, .micros = 164061 }, (try DateTime.fromUnix(-24631052202835939, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 23, .min = 2, .sec = 41, .micros = 147703 }, (try DateTime.fromUnix(27754959761147703, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 16, .min = 51, .sec = 1, .micros = 710888 }, (try DateTime.fromUnix(-29839475338289112, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 1, .min = 31, .sec = 44, .micros = 244667 }, (try DateTime.fromUnix(13143000704244667, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 14, .min = 40, .sec = 45, .micros = 594500 }, (try DateTime.fromUnix(-27029323154405500, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 3, .min = 28, .sec = 18, .micros = 941443 }, (try DateTime.fromUnix(26929337298941443, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 18, .min = 34, .sec = 26, .micros = 418287 }, (try DateTime.fromUnix(-16849401933581713, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 16, .min = 51, .sec = 12, .micros = 390293 }, (try DateTime.fromUnix(24013471872390293, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 27, .sec = 59, .micros = 116472 }, (try DateTime.fromUnix(-4881839520883528, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 38, .sec = 58, .micros = 829840 }, (try DateTime.fromUnix(28012689538829840, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 13, .min = 31, .sec = 51, .micros = 397163 }, (try DateTime.fromUnix(-14000034488602837, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 16, .min = 25, .sec = 36, .micros = 566333 }, (try DateTime.fromUnix(3819630336566333, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 23, .min = 52, .sec = 35, .micros = 404576 }, (try DateTime.fromUnix(-24790838844595424, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 14, .min = 17, .sec = 56, .micros = 248627 }, (try DateTime.fromUnix(4303462676248627, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 56, .sec = 31, .micros = 445770 }, (try DateTime.fromUnix(-7573827808554230, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 1, .min = 36, .sec = 32, .micros = 60901 }, (try DateTime.fromUnix(12791180192060901, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 4, .min = 12, .sec = 1, .micros = 816276 }, (try DateTime.fromUnix(-29726596078183724, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 25, .sec = 2, .micros = 88680 }, (try DateTime.fromUnix(9072494702088680, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 7, .min = 14, .sec = 18, .micros = 149127 }, (try DateTime.fromUnix(-20968821941850873, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 15, .min = 45, .sec = 55, .micros = 818121 }, (try DateTime.fromUnix(14590424755818121, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 13, .min = 45, .sec = 5, .micros = 544234 }, (try DateTime.fromUnix(-21099694494455766, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 20, .min = 58, .sec = 32, .micros = 361661 }, (try DateTime.fromUnix(27070837112361661, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 18, .min = 42, .sec = 3, .micros = 375293 }, (try DateTime.fromUnix(-22783699076624707, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 15, .min = 5, .sec = 18, .micros = 844868 }, (try DateTime.fromUnix(3924515118844868, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 39, .sec = 15, .micros = 454348 }, (try DateTime.fromUnix(-19519510844545652, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 34, .sec = 57, .micros = 584438 }, (try DateTime.fromUnix(25405223697584438, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 12, .min = 58, .sec = 48, .micros = 604253 }, (try DateTime.fromUnix(-23848167671395747, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 21, .min = 6, .sec = 10, .micros = 130143 }, (try DateTime.fromUnix(9179039170130143, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 11, .min = 40, .sec = 45, .micros = 806457 }, (try DateTime.fromUnix(-10457900354193543, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 32, .sec = 3, .micros = 84471 }, (try DateTime.fromUnix(20206560723084471, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 11, .min = 8, .sec = 48, .micros = 571978 }, (try DateTime.fromUnix(-13147966271428022, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 10, .min = 37, .sec = 9, .micros = 847397 }, (try DateTime.fromUnix(9639599829847397, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 20, .min = 15, .sec = 37, .micros = 731453 }, (try DateTime.fromUnix(-17972509462268547, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 0, .min = 36, .sec = 51, .micros = 658834 }, (try DateTime.fromUnix(23080639011658834, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 3, .min = 6, .sec = 2, .micros = 359939 }, (try DateTime.fromUnix(-13484004837640061, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 1, .min = 24, .sec = 8, .micros = 76822 }, (try DateTime.fromUnix(22642161848076822, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 20, .sec = 47, .micros = 940649 }, (try DateTime.fromUnix(-9576815952059351, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 16, .min = 19, .sec = 30, .micros = 228423 }, (try DateTime.fromUnix(11237847570228423, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 16, .min = 54, .sec = 33, .micros = 913828 }, (try DateTime.fromUnix(-9146156726086172, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 20, .min = 14, .sec = 10, .micros = 663120 }, (try DateTime.fromUnix(12400805650663120, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 15, .min = 22, .sec = 22, .micros = 500411 }, (try DateTime.fromUnix(-13183893457499589, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 18, .min = 42, .sec = 11, .micros = 637021 }, (try DateTime.fromUnix(17415888131637021, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 7, .sec = 43, .micros = 497651 }, (try DateTime.fromUnix(-3828045136502349, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 9, .min = 25, .sec = 22, .micros = 960397 }, (try DateTime.fromUnix(25585406722960397, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 20, .min = 36, .sec = 31, .micros = 312572 }, (try DateTime.fromUnix(-11209202608687428, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 5, .min = 25, .sec = 18, .micros = 104173 }, (try DateTime.fromUnix(7748544318104173, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 11, .min = 23, .sec = 25, .micros = 504363 }, (try DateTime.fromUnix(-22111446994495637, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 19, .min = 48, .sec = 44, .micros = 703684 }, (try DateTime.fromUnix(21347696924703684, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 12, .min = 10, .sec = 21, .micros = 67035 }, (try DateTime.fromUnix(-29976004178932965, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 6, .min = 0, .sec = 55, .micros = 355102 }, (try DateTime.fromUnix(15622869655355102, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 21, .min = 12, .sec = 1, .micros = 574873 }, (try DateTime.fromUnix(-28386384478425127, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 29, .sec = 45, .micros = 886627 }, (try DateTime.fromUnix(27787703385886627, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 8, .min = 43, .sec = 51, .micros = 403514 }, (try DateTime.fromUnix(-591981368596486, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 12, .min = 1, .sec = 19, .micros = 667089 }, (try DateTime.fromUnix(411998479667089, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 14, .min = 15, .sec = 53, .micros = 366760 }, (try DateTime.fromUnix(-29916899046633240, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 19, .min = 31, .sec = 23, .micros = 639485 }, (try DateTime.fromUnix(29847555083639485, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 0, .min = 21, .sec = 29, .micros = 207122 }, (try DateTime.fromUnix(-13356229110792878, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 10, .min = 35, .sec = 51, .micros = 789976 }, (try DateTime.fromUnix(2401353351789976, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 23, .min = 51, .sec = 4, .micros = 23674 }, (try DateTime.fromUnix(-8687002135976326, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 3, .min = 23, .sec = 21, .micros = 985741 }, (try DateTime.fromUnix(7637772201985741, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 22, .min = 3, .sec = 34, .micros = 497666 }, (try DateTime.fromUnix(-22331814985502334, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 15, .sec = 11, .micros = 818441 }, (try DateTime.fromUnix(14544983711818441, .microseconds)).time()); + try testing.expectEqual(Time{ .hour = 17, .min = 47, .sec = 39, .micros = 303089 }, (try DateTime.fromUnix(-19977775940696911, .microseconds)).time()); +} + +test "DateTime: parse RFC822" { + // try testing.expectError(error.InvalidDateTime, DateTime.parse("", .rfc822)); + // try testing.expectError(error.InvalidDateTime, DateTime.parse("nope", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Oth, 01 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Mon , 01 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Mon, 01 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse(" Mon, 1 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Wed, 1 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Wed, 01 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Wed, 01 J 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Wed, 01 Ja 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Wed, 01 Jan 2 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidDate, DateTime.parse("Wed, 01 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 10:10 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 1:10 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 a:10 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 1a:10 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 200:10 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:001 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:a Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a: Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:1 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:a Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:999 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:999 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:22", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:22 Z", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:22 X", .rfc822)); + try testing.expectError(error.InvalidTime, DateTime.parse("Wed, 01 Jan 20 20:1a:22 ZZ", .rfc822)); + + { + const dt = try DateTime.parse("31 Dec 68 23:59 Z", .rfc822); + try testing.expectEqual(3124223940000000, dt.micros); + try testing.expectEqual(2068, dt.date().year); + try testing.expectEqual(12, dt.date().month); + try testing.expectEqual(31, dt.date().day); + try testing.expectEqual(23, dt.time().hour); + try testing.expectEqual(59, dt.time().min); + try testing.expectEqual(0, dt.time().sec); + try testing.expectEqual(0, dt.time().micros); + } + + { + const dt = try DateTime.parse("Mon, 31 Dec 68 23:59 Z", .rfc822); + try testing.expectEqual(3124223940000000, dt.micros); + try testing.expectEqual(2068, dt.date().year); + try testing.expectEqual(12, dt.date().month); + try testing.expectEqual(31, dt.date().day); + try testing.expectEqual(23, dt.time().hour); + try testing.expectEqual(59, dt.time().min); + try testing.expectEqual(0, dt.time().sec); + try testing.expectEqual(0, dt.time().micros); + } + + { + const dt = try DateTime.parse("01 Jan 69 01:22:03 GMT", .rfc822); + try testing.expectEqual(-31531077000000, dt.micros); + try testing.expectEqual(1969, dt.date().year); + try testing.expectEqual(1, dt.date().month); + try testing.expectEqual(1, dt.date().day); + try testing.expectEqual(1, dt.time().hour); + try testing.expectEqual(22, dt.time().min); + try testing.expectEqual(3, dt.time().sec); + try testing.expectEqual(0, dt.time().micros); + } + + { + const dt = try DateTime.parse("Sat, 18 Jan 2070 01:22:03 GMT", .rfc822); + try testing.expectEqual(3157233723000000, dt.micros); + try testing.expectEqual(2070, dt.date().year); + try testing.expectEqual(1, dt.date().month); + try testing.expectEqual(18, dt.date().day); + try testing.expectEqual(1, dt.time().hour); + try testing.expectEqual(22, dt.time().min); + try testing.expectEqual(3, dt.time().sec); + try testing.expectEqual(0, dt.time().micros); + } +} + +test "DateTime: parse RFC3339" { + { + const dt = try DateTime.parse("-3221-01-02T03:04:05Z", .rfc3339); + try testing.expectEqual(-163812056155000000, dt.micros); + try testing.expectEqual(-3221, dt.date().year); + try testing.expectEqual(1, dt.date().month); + try testing.expectEqual(2, dt.date().day); + try testing.expectEqual(3, dt.time().hour); + try testing.expectEqual(4, dt.time().min); + try testing.expectEqual(5, dt.time().sec); + try testing.expectEqual(0, dt.time().micros); + } + + { + const dt = try DateTime.parse("0001-02-03T04:05:06.789+00:00", .rfc3339); + try testing.expectEqual(-62132730893211000, dt.micros); + try testing.expectEqual(1, dt.date().year); + try testing.expectEqual(2, dt.date().month); + try testing.expectEqual(3, dt.date().day); + try testing.expectEqual(4, dt.time().hour); + try testing.expectEqual(5, dt.time().min); + try testing.expectEqual(6, dt.time().sec); + try testing.expectEqual(789000, dt.time().micros); + } + + { + const dt = try DateTime.parse("5000-12-31T23:59:58.987654321Z", .rfc3339); + try testing.expectEqual(95649119998987654, dt.micros); + try testing.expectEqual(5000, dt.date().year); + try testing.expectEqual(12, dt.date().month); + try testing.expectEqual(31, dt.date().day); + try testing.expectEqual(23, dt.time().hour); + try testing.expectEqual(59, dt.time().min); + try testing.expectEqual(58, dt.time().sec); + try testing.expectEqual(987654, dt.time().micros); + } + + { + // invalid format + try testing.expectError(error.InvalidDate, DateTime.parse("", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023/01-02T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-01/02T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDateTime, DateTime.parse("0001-01-01 T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDateTime, DateTime.parse("0001-01-01t00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDateTime, DateTime.parse("0001-01-01 00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-1-02T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-01-2T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("9-01-2T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("99-01-2T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("999-01-2T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("-999-01-2T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("-1-01-2T00:00Z", .rfc3339)); + } + + // date portion is ISO8601 + try testing.expectError(error.InvalidDate, DateTime.parse("20230102T23:59:58.987654321Z", .rfc3339)); + + { + // invalid month + try testing.expectError(error.InvalidDate, DateTime.parse("2023-00-22T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-0A-22T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-13-22T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-99-22T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("-2023-00-22T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("-2023-13-22T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("-2023-99-22T00:00Z", .rfc3339)); + } + + { + // invalid day + try testing.expectError(error.InvalidDate, DateTime.parse("2023-01-00T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-01-32T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-02-29T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-03-32T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-04-31T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-05-32T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-06-31T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-07-32T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-08-32T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-09-31T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-10-32T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-11-31T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2023-12-32T00:00Z", .rfc3339)); + } + + { + // valid (max day) + try testing.expectEqual(1675123200000000, (try DateTime.parse("2023-01-31T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1677542400000000, (try DateTime.parse("2023-02-28T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1680220800000000, (try DateTime.parse("2023-03-31T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1682812800000000, (try DateTime.parse("2023-04-30T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1685491200000000, (try DateTime.parse("2023-05-31T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1688083200000000, (try DateTime.parse("2023-06-30T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1690761600000000, (try DateTime.parse("2023-07-31T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1693440000000000, (try DateTime.parse("2023-08-31T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1696032000000000, (try DateTime.parse("2023-09-30T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1698710400000000, (try DateTime.parse("2023-10-31T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1701302400000000, (try DateTime.parse("2023-11-30T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1703980800000000, (try DateTime.parse("2023-12-31T00:00Z", .rfc3339)).micros); + } + + { + // leap years + try testing.expectEqual(951782400000000, (try DateTime.parse("2000-02-29T00:00Z", .rfc3339)).micros); + try testing.expectEqual(13574563200000000, (try DateTime.parse("2400-02-29T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1330473600000000, (try DateTime.parse("2012-02-29T00:00Z", .rfc3339)).micros); + try testing.expectEqual(1709164800000000, (try DateTime.parse("2024-02-29T00:00Z", .rfc3339)).micros); + + try testing.expectError(error.InvalidDate, DateTime.parse("2000-02-30T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2400-02-30T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2012-02-30T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2024-02-30T00:00Z", .rfc3339)); + + try testing.expectError(error.InvalidDate, DateTime.parse("2100-02-29T00:00Z", .rfc3339)); + try testing.expectError(error.InvalidDate, DateTime.parse("2200-02-29T00:00Z", .rfc3339)); + } + + { + // invalid time + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T01:00:", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T1:00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T10:1:00", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T10:11:4", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T10:20:30.", .rfc3339)); + try testing.expectError(error.InvalidDateTime, DateTime.parse("2023-10-10T10:20:30.a", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T10:20:30.1234567899", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T24:00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T00:60:00", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T00:00:60", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T0a:00:00", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T00:0a:00", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T00:00:0a", .rfc3339)); + try testing.expectError(error.InvalidTime, DateTime.parse("2023-10-10T00/00:00", .rfc3339)); + try testing.expectError(error.InvalidDateTime, DateTime.parse("2023-10-10T00:00 00", .rfc3339)); + } +} + +test "DateTime: json" { + { + // DateTime, time no fraction + const dt = try DateTime.parse("2023-09-22T23:59:02Z", .rfc3339); + const out = try std.json.stringifyAlloc(testing.allocator, dt, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"2023-09-22T23:59:02Z\"", out); + } + + { + // time, milliseconds only + const dt = try DateTime.parse("2023-09-22T07:09:32.202Z", .rfc3339); + const out = try std.json.stringifyAlloc(testing.allocator, dt, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"2023-09-22T07:09:32.202Z\"", out); + } + + { + // time, micros + const dt = try DateTime.parse("-0004-12-03T01:02:03.123456Z", .rfc3339); + const out = try std.json.stringifyAlloc(testing.allocator, dt, .{}); + defer testing.allocator.free(out); + try testing.expectString("\"-0004-12-03T01:02:03.123456Z\"", out); + } + + { + // parse + const ts = try std.json.parseFromSlice(TestStruct, testing.allocator, "{\"datetime\":\"2023-09-22T07:09:32.202Z\"}", .{}); + defer ts.deinit(); + try testing.expectEqual(try DateTime.parse("2023-09-22T07:09:32.202Z", .rfc3339), ts.value.datetime.?); + } +} + +test "DateTime: format" { + { + var buf: [30]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{try DateTime.initUTC(2023, 5, 22, 23, 59, 59, 0)}); + try testing.expectString("2023-05-22T23:59:59Z", out); + } + + { + var buf: [30]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{try DateTime.initUTC(2023, 5, 22, 8, 9, 10, 12)}); + try testing.expectString("2023-05-22T08:09:10.000012Z", out); + } + + { + var buf: [30]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{try DateTime.initUTC(2023, 5, 22, 8, 9, 10, 123)}); + try testing.expectString("2023-05-22T08:09:10.000123Z", out); + } + + { + var buf: [30]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{try DateTime.initUTC(2023, 5, 22, 8, 9, 10, 1234)}); + try testing.expectString("2023-05-22T08:09:10.001234Z", out); + } + + { + var buf: [30]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{try DateTime.initUTC(-102, 12, 9, 8, 9, 10, 12345)}); + try testing.expectString("-0102-12-09T08:09:10.012345Z", out); + } + + { + var buf: [30]u8 = undefined; + const out = try std.fmt.bufPrint(&buf, "{s}", .{try DateTime.initUTC(-102, 12, 9, 8, 9, 10, 123456)}); + try testing.expectString("-0102-12-09T08:09:10.123456Z", out); + } +} + +test "DateTime: order" { + { + const a = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002); + const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002); + try testing.expectEqual(std.math.Order.eq, a.order(b)); + } + + { + const a = try DateTime.initUTC(2023, 5, 22, 12, 59, 2, 492); + const b = try DateTime.initUTC(2022, 5, 22, 23, 59, 2, 492); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = try DateTime.initUTC(2022, 6, 22, 23, 59, 2, 492); + const b = try DateTime.initUTC(2022, 5, 22, 23, 33, 2, 492); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = try DateTime.initUTC(2023, 5, 23, 23, 59, 2, 492); + const b = try DateTime.initUTC(2022, 5, 22, 23, 59, 11, 492); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = try DateTime.initUTC(2023, 11, 23, 20, 17, 22, 101002); + const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = try DateTime.initUTC(2023, 11, 23, 19, 18, 22, 101002); + const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = try DateTime.initUTC(2023, 11, 23, 19, 17, 23, 101002); + const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } + + { + const a = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101003); + const b = try DateTime.initUTC(2023, 11, 23, 19, 17, 22, 101002); + try testing.expectEqual(std.math.Order.gt, a.order(b)); + try testing.expectEqual(std.math.Order.lt, b.order(a)); + } +} + +test "DateTime: unix" { + { + const dt = try DateTime.initUTC(-4322, 1, 1, 0, 0, 0, 0); + try testing.expectEqual(-198556272000, dt.unix(.seconds)); + try testing.expectEqual(-198556272000000, dt.unix(.milliseconds)); + try testing.expectEqual(-198556272000000000, dt.unix(.microseconds)); + } + + { + const dt = try DateTime.initUTC(1970, 1, 1, 0, 0, 0, 0); + try testing.expectEqual(0, dt.unix(.seconds)); + try testing.expectEqual(0, dt.unix(.milliseconds)); + try testing.expectEqual(0, dt.unix(.microseconds)); + } + + { + const dt = try DateTime.initUTC(2023, 11, 24, 12, 6, 14, 918000); + try testing.expectEqual(1700827574, dt.unix(.seconds)); + try testing.expectEqual(1700827574918, dt.unix(.milliseconds)); + try testing.expectEqual(1700827574918000, dt.unix(.microseconds)); + } + + // microseconds + // GO: + // for i := 0; i < 50; i++ { + // us := rand.Int63n(3153600000000000) + // if i%2 == 1 { + // us = -us + // } + // date := time.UnixMicro(us).UTC() + // fmt.Printf("\ttry testing.expectEqual(%d, (try DateTime.parse(\"%s\", .rfc3339)).unix(.microseconds));\n", us, date.Format(time.RFC3339Nano)) + // } + try testing.expectEqual(2568689002670356, (try DateTime.parse("2051-05-26T04:43:22.670356Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2994122503199268, (try DateTime.parse("1875-02-13T19:18:16.800732Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2973860981156244, (try DateTime.parse("2064-03-27T16:29:41.156244Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2122539648627924, (try DateTime.parse("1902-09-28T13:39:11.372076Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1440540448439442, (try DateTime.parse("2015-08-25T22:07:28.439442Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-843471236299718, (try DateTime.parse("1943-04-10T14:26:03.700282Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2428009970341301, (try DateTime.parse("2046-12-09T23:12:50.341301Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-861640488391156, (try DateTime.parse("1942-09-12T07:25:11.608844Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(107457228254516, (try DateTime.parse("1973-05-28T17:13:48.254516Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-858997335483954, (try DateTime.parse("1942-10-12T21:37:44.516046Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1879201014676957, (try DateTime.parse("2029-07-20T00:16:54.676957Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2779215184508509, (try DateTime.parse("1881-12-06T03:46:55.491491Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(790920073212180, (try DateTime.parse("1995-01-24T04:01:13.21218Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-1986764905311346, (try DateTime.parse("1907-01-17T00:51:34.688654Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1567001594851223, (try DateTime.parse("2019-08-28T14:13:14.851223Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2786308994565191, (try DateTime.parse("1881-09-15T01:16:45.434809Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1190930851203854, (try DateTime.parse("2007-09-27T22:07:31.203854Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-13894507787609, (try DateTime.parse("1969-07-24T04:24:52.212391Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1283185581222987, (try DateTime.parse("2010-08-30T16:26:21.222987Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-3080071240438154, (try DateTime.parse("1872-05-25T00:39:19.561846Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(3091078494301752, (try DateTime.parse("2067-12-14T08:54:54.301752Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2788286096253476, (try DateTime.parse("1881-08-23T04:05:03.746524Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1226140349962650, (try DateTime.parse("2008-11-08T10:32:29.96265Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-173789078990530, (try DateTime.parse("1964-06-29T13:15:21.00947Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2202006978733437, (try DateTime.parse("2039-10-12T04:36:18.733437Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-1957390566907891, (try DateTime.parse("1907-12-23T00:23:53.092109Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2704228013874812, (try DateTime.parse("2055-09-10T22:26:53.874812Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2162891323622724, (try DateTime.parse("1901-06-18T12:51:16.377276Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2985526644225853, (try DateTime.parse("2064-08-09T16:57:24.225853Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2714126911982044, (try DateTime.parse("1883-12-29T11:51:28.017956Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1389358847381035, (try DateTime.parse("2014-01-10T13:00:47.381035Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2599632972496238, (try DateTime.parse("1887-08-15T15:43:47.503762Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2842567982275671, (try DateTime.parse("2060-01-29T02:13:02.275671Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2924719405531619, (try DateTime.parse("1877-04-27T01:56:34.468381Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(929389345478708, (try DateTime.parse("1999-06-14T19:42:25.478708Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2928161617689577, (try DateTime.parse("1877-03-18T05:46:22.310423Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1981926664387480, (try DateTime.parse("2032-10-20T23:11:04.38748Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-3077852548046313, (try DateTime.parse("1872-06-19T16:57:31.953687Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(323327680783683, (try DateTime.parse("1980-03-31T05:14:40.783683Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-1282955701919591, (try DateTime.parse("1929-05-06T23:24:58.080409Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(1382921217423641, (try DateTime.parse("2013-10-28T00:46:57.423641Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-1431006940775286, (try DateTime.parse("1924-08-27T10:04:19.224714Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(3074639946025509, (try DateTime.parse("2067-06-07T02:39:06.025509Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2634608860053384, (try DateTime.parse("1886-07-06T20:12:19.946616Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2779915686281386, (try DateTime.parse("2058-02-02T22:48:06.281386Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2016252325938190, (try DateTime.parse("1906-02-09T17:54:34.06181Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(342848400150959, (try DateTime.parse("1980-11-12T03:40:00.150959Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-2645960576992651, (try DateTime.parse("1886-02-25T10:57:03.007349Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(2460926767780856, (try DateTime.parse("2047-12-25T22:46:07.780856Z", .rfc3339)).unix(.microseconds)); + try testing.expectEqual(-3072719558320472, (try DateTime.parse("1872-08-18T02:47:21.679528Z", .rfc3339)).unix(.microseconds)); + + // milliseconds + // GO + // for i := 0; i < 50; i++ { + // us := rand.Int63n(3153600000000000) + // if i%2 == 1 { + // us = -us + // } + // date := time.UnixMicro(us).UTC() + // fmt.Printf("\ttry testing.expectEqual(%d, (try DateTime.parse(\"%s\", .rfc3339)).unix(.milliseconds));\n", us/1000, date.Format(time.RFC3339Nano)) + // } + try testing.expectEqual(1397526377500, (try DateTime.parse("2014-04-15T01:46:17.500928Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-586731476093, (try DateTime.parse("1951-05-30T03:02:03.906951Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2626709817261, (try DateTime.parse("2053-03-27T17:36:57.261986Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2699459388451, (try DateTime.parse("1884-06-16T06:10:11.548899Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(187068511670, (try DateTime.parse("1975-12-06T03:28:31.670454Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-785593098555, (try DateTime.parse("1945-02-08T11:41:41.444519Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2482013929293, (try DateTime.parse("2048-08-26T00:18:49.293566Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-39404841784, (try DateTime.parse("1968-10-01T22:12:38.215367Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(1534769380821, (try DateTime.parse("2018-08-20T12:49:40.821612Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1980714497790, (try DateTime.parse("1907-03-28T01:31:42.209908Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(1981870811721, (try DateTime.parse("2032-10-20T07:40:11.721424Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-554657243269, (try DateTime.parse("1952-06-04T08:32:36.730587Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(78531146024, (try DateTime.parse("1972-06-27T22:12:26.024177Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2360798362731, (try DateTime.parse("1895-03-10T22:40:37.268319Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2843392029355, (try DateTime.parse("2060-02-07T15:07:09.355931Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1289360209568, (try DateTime.parse("1929-02-21T20:23:10.431793Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2440116994057, (try DateTime.parse("2047-04-29T02:16:34.057859Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1958937239211, (try DateTime.parse("1907-12-05T02:46:00.788847Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2092930144205, (try DateTime.parse("2036-04-27T17:29:04.205599Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1314934006371, (try DateTime.parse("1928-05-01T20:33:13.628366Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(1987707686213, (try DateTime.parse("2032-12-26T21:01:26.21383Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2863567343704, (try DateTime.parse("1879-04-04T20:37:36.295226Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(1776340450602, (try DateTime.parse("2026-04-16T11:54:10.602059Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-135109264096, (try DateTime.parse("1965-09-20T05:38:55.903281Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(664556549013, (try DateTime.parse("1991-01-22T15:02:29.013079Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1265741428742, (try DateTime.parse("1929-11-22T05:09:31.257333Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(677440942549, (try DateTime.parse("1991-06-20T18:02:22.549734Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-3086845293210, (try DateTime.parse("1872-03-07T14:58:26.789666Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2662366721158, (try DateTime.parse("2054-05-14T10:18:41.158507Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-35310777646, (try DateTime.parse("1968-11-18T07:27:02.353055Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(466748318057, (try DateTime.parse("1984-10-16T04:18:38.057985Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1142849776788, (try DateTime.parse("1933-10-14T13:43:43.211425Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(299657172861, (try DateTime.parse("1979-07-01T06:06:12.86151Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2674956599650, (try DateTime.parse("1885-03-26T20:30:00.34904Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2608306771546, (try DateTime.parse("2052-08-26T17:39:31.546441Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2890194900832, (try DateTime.parse("1878-05-31T16:04:59.167405Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(396552033685, (try DateTime.parse("1982-07-26T17:20:33.68525Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-107099840493, (try DateTime.parse("1966-08-10T10:02:39.506219Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(3003275118291, (try DateTime.parse("2065-03-03T03:05:18.291675Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1827348315834, (try DateTime.parse("1912-02-05T03:14:44.165534Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(276927903561, (try DateTime.parse("1978-10-11T04:25:03.561761Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2769749223625, (try DateTime.parse("1882-03-25T17:12:56.374223Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2626498021199, (try DateTime.parse("2053-03-25T06:47:01.199662Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-1394547124859, (try DateTime.parse("1925-10-23T09:47:55.140254Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(272330504585, (try DateTime.parse("1978-08-18T23:21:44.585364Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2210407675350, (try DateTime.parse("1899-12-15T13:52:04.649158Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(1506546882755, (try DateTime.parse("2017-09-27T21:14:42.755649Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-2320627977264, (try DateTime.parse("1896-06-17T21:07:02.735544Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(2719300156090, (try DateTime.parse("2056-03-03T09:09:16.090337Z", .rfc3339)).unix(.milliseconds)); + try testing.expectEqual(-450791776320, (try DateTime.parse("1955-09-19T12:03:43.679144Z", .rfc3339)).unix(.milliseconds)); + + // seconds + // GO + // for i := 0; i < 50; i++ { + // us := rand.Int63n(3153600000000000) + // if i%2 == 1 { + // us = -us + // } + // date := time.UnixMicro(us).UTC() + // fmt.Printf("\ttry testing.expectEqual(%d, (try DateTime.parse(\"%s\", .rfc3339)).unix(.milliseconds));\n", us/1000/1000, date.Format(time.RFC3339Nano)) + // } + try testing.expectEqual(1019355037, (try DateTime.parse("2002-04-21T02:10:37.264298Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2639191098, (try DateTime.parse("1886-05-14T19:21:41.481076Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(552479765, (try DateTime.parse("1987-07-05T10:36:05.374475Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2842270449, (try DateTime.parse("1879-12-07T08:25:50.857157Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2287542812, (try DateTime.parse("2042-06-28T04:33:32.585424Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1032056861, (try DateTime.parse("1937-04-18T21:32:18.185245Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2294125759, (try DateTime.parse("2042-09-12T09:09:19.324234Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2434666174, (try DateTime.parse("1892-11-05T23:50:25.855342Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2130180824, (try DateTime.parse("2037-07-02T20:53:44.663679Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2088926942, (try DateTime.parse("1903-10-22T14:30:57.110159Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(210188161, (try DateTime.parse("1976-08-29T17:36:01.512348Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1594811550, (try DateTime.parse("1919-06-19T12:47:29.692995Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(408055212, (try DateTime.parse("1982-12-06T20:40:12.74791Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-763370385, (try DateTime.parse("1945-10-23T16:40:14.54824Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2220686606, (try DateTime.parse("2040-05-15T09:23:26.183323Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1829267394, (try DateTime.parse("1912-01-13T22:10:05.152891Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(186103622, (try DateTime.parse("1975-11-24T23:27:02.092278Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-104963797, (try DateTime.parse("1966-09-04T03:23:22.379643Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(188664629, (try DateTime.parse("1975-12-24T14:50:29.082285Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-978305356, (try DateTime.parse("1939-01-01T00:30:43.460779Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(1857079750, (try DateTime.parse("2028-11-05T23:29:10.225783Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1059764722, (try DateTime.parse("1936-06-02T04:54:37.841836Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2931563560, (try DateTime.parse("2062-11-24T03:12:40.682221Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-58861051, (try DateTime.parse("1968-02-19T17:42:28.861019Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2540374023, (try DateTime.parse("2050-07-02T11:27:03.083527Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-369803898, (try DateTime.parse("1958-04-13T20:41:41.391534Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(1150522786, (try DateTime.parse("2006-06-17T05:39:46.776689Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-3094311182, (try DateTime.parse("1871-12-12T05:06:57.955425Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2742945297, (try DateTime.parse("2056-12-02T01:14:57.552041Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-3055421456, (try DateTime.parse("1873-03-06T07:49:03.861761Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(1935913185, (try DateTime.parse("2031-05-07T09:39:45.408961Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1546803921, (try DateTime.parse("1920-12-26T04:14:38.089431Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2430955251, (try DateTime.parse("2047-01-13T01:20:51.611416Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1162742133, (try DateTime.parse("1933-02-26T08:04:26.776057Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2820984010, (try DateTime.parse("2059-05-24T06:40:10.9707Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2671779872, (try DateTime.parse("1885-05-02T14:55:27.010415Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(419726969, (try DateTime.parse("1983-04-20T22:49:29.184213Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2886236400, (try DateTime.parse("1878-07-16T11:39:59.700923Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(1091845921, (try DateTime.parse("2004-08-07T02:32:01.949043Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-1345585389, (try DateTime.parse("1927-05-13T02:16:50.807413Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(968555612, (try DateTime.parse("2000-09-10T03:13:32.056103Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-525723150, (try DateTime.parse("1953-05-05T05:47:29.657935Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2179443523, (try DateTime.parse("2039-01-24T00:58:43.238504Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2200838901, (try DateTime.parse("1900-04-05T07:51:38.801707Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(567335109, (try DateTime.parse("1987-12-24T09:05:09.535877Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-714932675, (try DateTime.parse("1947-05-07T07:35:24.863781Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(2735649359, (try DateTime.parse("2056-09-08T14:35:59.483204Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-2386101706, (try DateTime.parse("1894-05-22T01:58:13.445088Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(115985094, (try DateTime.parse("1973-09-04T10:04:54.005266Z", .rfc3339)).unix(.seconds)); + try testing.expectEqual(-3046532170, (try DateTime.parse("1873-06-17T05:03:49.260067Z", .rfc3339)).unix(.seconds)); +} + +test "DateTime: limits" { + { + // min + const dt1 = try DateTime.initUTC(-4712, 1, 1, 0, 0, 0, 0); + const dt2 = try DateTime.parse("-4712-01-01T00:00:00.000000Z", .rfc3339); + const dt3 = try DateTime.fromUnix(-210863520000, .seconds); + const dt4 = try DateTime.fromUnix(-210863520000000, .milliseconds); + const dt5 = try DateTime.fromUnix(-210863520000000000, .microseconds); + for ([_]DateTime{ dt1, dt2, dt3, dt4, dt5 }) |dt| { + try testing.expectEqual(-4712, dt.date().year); + try testing.expectEqual(1, dt.date().month); + try testing.expectEqual(1, dt.date().day); + try testing.expectEqual(0, dt.time().hour); + try testing.expectEqual(0, dt.time().min); + try testing.expectEqual(0, dt.time().sec); + try testing.expectEqual(0, dt.time().micros); + try testing.expectEqual(-210863520000, dt.unix(.seconds)); + try testing.expectEqual(-210863520000000, dt.unix(.milliseconds)); + try testing.expectEqual(-210863520000000000, dt.unix(.microseconds)); + } + } + + { + // max + const dt1 = try DateTime.initUTC(9999, 12, 31, 23, 59, 59, 999999); + const dt2 = try DateTime.parse("9999-12-31T23:59:59.999999Z", .rfc3339); + const dt3 = try DateTime.fromUnix(253402300799, .seconds); + const dt4 = try DateTime.fromUnix(253402300799999, .milliseconds); + const dt5 = try DateTime.fromUnix(253402300799999999, .microseconds); + for ([_]DateTime{ dt1, dt2, dt3, dt4, dt5 }, 0..) |dt, i| { + try testing.expectEqual(9999, dt.date().year); + try testing.expectEqual(12, dt.date().month); + try testing.expectEqual(31, dt.date().day); + try testing.expectEqual(23, dt.time().hour); + try testing.expectEqual(59, dt.time().min); + try testing.expectEqual(59, dt.time().sec); + + try testing.expectEqual(253402300799, dt.unix(.seconds)); + + if (i == 2) { + try testing.expectEqual(0, dt.time().micros); + try testing.expectEqual(253402300799000, dt.unix(.milliseconds)); + try testing.expectEqual(253402300799000000, dt.unix(.microseconds)); + } else if (i == 3) { + try testing.expectEqual(999000, dt.time().micros); + try testing.expectEqual(253402300799999, dt.unix(.milliseconds)); + try testing.expectEqual(253402300799999000, dt.unix(.microseconds)); + } else { + try testing.expectEqual(999999, dt.time().micros); + try testing.expectEqual(253402300799999, dt.unix(.milliseconds)); + try testing.expectEqual(253402300799999999, dt.unix(.microseconds)); + } + } + } +} + +test "DateTime: add" { + { + // positive + var dt = try DateTime.parse("2023-11-26T03:13:46.540234Z", .rfc3339); + + dt = try dt.add(800, .microseconds); + try expectDateTime("2023-11-26T03:13:46.541034Z", dt); + + dt = try dt.add(950, .milliseconds); + try expectDateTime("2023-11-26T03:13:47.491034Z", dt); + + dt = try dt.add(32, .seconds); + try expectDateTime("2023-11-26T03:14:19.491034Z", dt); + + dt = try dt.add(1489, .minutes); + try expectDateTime("2023-11-27T04:03:19.491034Z", dt); + + dt = try dt.add(6, .days); + try expectDateTime("2023-12-03T04:03:19.491034Z", dt); + } + + { + // negative + var dt = try DateTime.parse("2023-11-26T03:13:46.540234Z", .rfc3339); + + dt = try dt.add(-800, .microseconds); + try expectDateTime("2023-11-26T03:13:46.539434Z", dt); + + dt = try dt.add(-950, .milliseconds); + try expectDateTime("2023-11-26T03:13:45.589434Z", dt); + + dt = try dt.add(-50, .seconds); + try expectDateTime("2023-11-26T03:12:55.589434Z", dt); + + dt = try dt.add(-1489, .minutes); + try expectDateTime("2023-11-25T02:23:55.589434Z", dt); + + dt = try dt.add(-6, .days); + try expectDateTime("2023-11-19T02:23:55.589434Z", dt); + } +} + +test "DateTime: sub" { + { + const a = try DateTime.parse("2023-11-26T03:13:46.540234Z", .rfc3339); + const b = try DateTime.parse("2023-11-26T03:13:47.540236Z", .rfc3339); + + try testing.expectEqual(-1, a.sub(b, .seconds)); + try testing.expectEqual(-1000, a.sub(b, .milliseconds)); + try testing.expectEqual(-1000002, a.sub(b, .microseconds)); + } + + { + const a = try DateTime.parse("2023-11-27T03:13:47.540234Z", .rfc3339); + const b = try DateTime.parse("2023-11-26T03:13:47.540234Z", .rfc3339); + + try testing.expectEqual(86400, a.sub(b, .seconds)); + try testing.expectEqual(86400000, a.sub(b, .milliseconds)); + try testing.expectEqual(86400000000, a.sub(b, .microseconds)); + } +} + +fn expectDateTime(expected: []const u8, dt: DateTime) !void { + var buf: [30]u8 = undefined; + const actual = try std.fmt.bufPrint(&buf, "{s}", .{dt}); + try testing.expectString(expected, actual); +} + +const TestStruct = struct { + date: ?Date = null, + time: ?Time = null, + datetime: ?DateTime = null, +}; diff --git a/src/storage/cookie.zig b/src/storage/cookie.zig new file mode 100644 index 000000000..e9a8a30e8 --- /dev/null +++ b/src/storage/cookie.zig @@ -0,0 +1,421 @@ +const std = @import("std"); +const Uri = std.Uri; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const DateTime = @import("../datetime.zig").DateTime; + +pub const Cookie = struct { + arena: ArenaAllocator, + name: []const u8, + value: []const u8, + path: []const u8, + domain: []const u8, + expires: ?i64, + secure: bool, + http_only: bool, + same_site: SameSite, + + const SameSite = enum { + strict, + lax, + none, + }; + + pub fn deinit(self: *const Cookie) void { + self.arena.deinit(); + } + + // There's https://datatracker.ietf.org/doc/html/rfc6265 but browsers are + // far less strict. I only found 2 cases where browsers will reject a cookie: + // - a byte 0...32 and 127..255 anywhere in the cookie (the HTTP header + // parser might take care of this already) + // - any shenanigans with the domain attribute - it has to be the current + // domain or one of higher order, exluding TLD. + // Anything else, will turn into a cookie. + // Single value? That's a cookie with an emtpy name and a value + // Key or Values with characters the RFC says aren't allowed? Allowed! ( + // (as long as the characters are 32...126) + // Invalid attributes? Ignored. + // Invalid attribute values? Ignore. + // Duplicate attributes - use the last valid + // Value-less attributes with a value? Ignore the value + pub fn parse(allocator: Allocator, uri: std.Uri, str: []const u8) !Cookie { + if (str.len == 0) { + // this check is necessary, `std.mem.minMax` asserts len > 0 + return error.Empty; + } + + const host = (uri.host orelse return error.InvalidURI).percent_encoded; + + { + const min, const max = std.mem.minMax(u8, str); + if (min < 32 or max > 126) { + return error.InvalidByteSequence; + } + } + + var arena = ArenaAllocator.init(allocator); + errdefer arena.deinit(); + + const cookie_name, const cookie_value, const rest = parseNameValue(str) catch { + return error.InvalidNameValue; + }; + + var scrap: [8]u8 = undefined; + + var path: ?[]const u8 = null; + var domain: ?[]const u8 = null; + var secure: ?bool = null; + var max_age: ?i64 = null; + var http_only: ?bool = null; + var expires: ?DateTime = null; + var same_site: ?Cookie.SameSite = null; + + var it = std.mem.splitScalar(u8, rest, ';'); + while (it.next()) |attribute| { + const sep = std.mem.indexOfScalarPos(u8, attribute, 0, '=') orelse attribute.len; + const key_string = trim(attribute[0..sep]); + + if (key_string.len > 8) { + // not valid, ignore + continue; + } + + // Make sure no one changes our max length without also expanding the size of scrap + std.debug.assert(key_string.len <= 8); + + const key = std.meta.stringToEnum(enum { + path, + domain, + secure, + @"max-age", + expires, + httponly, + samesite, + }, std.ascii.lowerString(&scrap, key_string)) orelse continue; + + var value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]); + switch (key) { + .path => { + // path attribute value either begins with a '/' or we + // ignore it and use the "default-path" algorithm + if (value.len > 0 and value[0] == '/') { + path = value; + } + }, + .domain => { + if (value.len == 0) { + continue; + } + if (value[0] == '.') { + // leading dot is ignored + value = value[1..]; + } + + if (std.mem.indexOfScalarPos(u8, value, 0, '.') == null) { + // can't set a cookie for a TLD + return error.InvalidDomain; + } + + if (std.mem.endsWith(u8, host, value) == false) { + return error.InvalidDomain; + } + domain = value; + }, + .secure => secure = true, + .@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue, + .expires => expires = DateTime.parse(value, .rfc822) catch continue, + .httponly => http_only = true, + .samesite => { + same_site = std.meta.stringToEnum(Cookie.SameSite, std.ascii.lowerString(&scrap, value)) orelse continue; + }, + } + } + + const aa = arena.allocator(); + const owned_name = try aa.dupe(u8, cookie_name); + const owned_value = try aa.dupe(u8, cookie_value); + const owned_path = if (path) |p| + try aa.dupe(u8, p) + else + try defaultPath(aa, uri.path.percent_encoded); + + const owned_domain = if (domain) |d| blk: { + const s = try aa.alloc(u8, d.len + 1); + s[0] = '.'; + @memcpy(s[1..], d); + break :blk s; + } else blk: { + break :blk try aa.dupe(u8, host); + }; + + var normalized_expires: ?i64 = null; + if (max_age) |ma| { + normalized_expires = std.time.timestamp() + ma; + } else { + // max age takes priority over expires + if (expires) |e| { + normalized_expires = e.sub(DateTime.now(), .seconds); + } + } + + return .{ + .arena = arena, + .name = owned_name, + .value = owned_value, + .path = owned_path, + .same_site = same_site orelse .lax, + .secure = secure orelse false, + .http_only = http_only orelse false, + .domain = owned_domain, + .expires = normalized_expires, + }; + } + + fn parseNameValue(str: []const u8) !struct { []const u8, []const u8, []const u8 } { + const key_value_end = std.mem.indexOfScalarPos(u8, str, 0, ';') orelse str.len; + const rest = if (key_value_end == str.len) "" else str[key_value_end + 1 ..]; + + const sep = std.mem.indexOfScalarPos(u8, str[0..key_value_end], 0, '=') orelse { + const value = trim(str[0..key_value_end]); + if (value.len == 0) { + return error.Empty; + } + return .{ "", value, rest }; + }; + + const name = trim(str[0..sep]); + const value = trim(str[sep + 1 .. key_value_end]); + return .{ name, value, rest }; + } +}; + +fn defaultPath(allocator: Allocator, document_path: []const u8) ![]const u8 { + if (document_path.len == 0 or (document_path.len == 1 and document_path[0] == '/')) { + return "/"; + } + const last = std.mem.lastIndexOfScalar(u8, document_path[1..], '/') orelse { + return "/"; + }; + return try allocator.dupe(u8, document_path[0 .. last + 1]); +} + +fn trim(str: []const u8) []const u8 { + return std.mem.trim(u8, str, &std.ascii.whitespace); +} + +fn trimLeft(str: []const u8) []const u8 { + return std.mem.trimLeft(u8, str, &std.ascii.whitespace); +} + +fn trimRight(str: []const u8) []const u8 { + return std.mem.trimLeft(u8, str, &std.ascii.whitespace); +} + +const testing = @import("../testing.zig"); +test "Cookie: parse key=value" { + try expectError(error.Empty, null, ""); + try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' }); + try expectError(error.InvalidByteSequence, null, &.{ 'a', 127, '=', 'b' }); + try expectError(error.InvalidByteSequence, null, &.{ 'a', '=', 'b', 20 }); + try expectError(error.InvalidByteSequence, null, &.{ 'a', '=', 'b', 128 }); + + try expectAttribute(.{ .name = "", .value = "a" }, null, "a"); + try expectAttribute(.{ .name = "", .value = "a" }, null, "a;"); + try expectAttribute(.{ .name = "", .value = "a b" }, null, "a b"); + try expectAttribute(.{ .name = "a b", .value = "b" }, null, "a b=b"); + try expectAttribute(.{ .name = "a,", .value = "b" }, null, "a,=b"); + try expectAttribute(.{ .name = ":a>", .value = "b>><" }, null, ":a>=b>><"); + + try expectAttribute(.{ .name = "abc", .value = "" }, null, "abc="); + try expectAttribute(.{ .name = "abc", .value = "" }, null, "abc=;"); + + try expectAttribute(.{ .name = "a", .value = "b" }, null, "a=b"); + try expectAttribute(.{ .name = "a", .value = "b" }, null, "a=b;"); + + try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f"); + try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f "); + try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f;"); + try expectAttribute(.{ .name = "abc", .value = "fe f" }, null, "abc= fe f ;"); + try expectAttribute(.{ .name = "abc", .value = "\" fe f\"" }, null, "abc=\" fe f\""); + try expectAttribute(.{ .name = "abc", .value = "\" fe f \"" }, null, "abc=\" fe f \""); + try expectAttribute(.{ .name = "ab4344c", .value = "1ads23" }, null, " ab4344c=1ads23 "); + + try expectAttribute(.{ .name = "ab4344c", .value = "1ads23" }, null, " ab4344c = 1ads23 ;"); +} + +test "Cookie: parse path" { + try expectAttribute(.{ .path = "/" }, "http://a/", "b"); + try expectAttribute(.{ .path = "/" }, "http://a/", "b;path"); + try expectAttribute(.{ .path = "/" }, "http://a/", "b;Path="); + try expectAttribute(.{ .path = "/" }, "http://a/", "b;Path=;"); + try expectAttribute(.{ .path = "/" }, "http://a/", "b; Path=other"); + try expectAttribute(.{ .path = "/" }, "http://a/23", "b; path=other "); + + try expectAttribute(.{ .path = "/" }, "http://a/abc", "b"); + try expectAttribute(.{ .path = "/abc" }, "http://a/abc/", "b"); + try expectAttribute(.{ .path = "/abc" }, "http://a/abc/123", "b"); + try expectAttribute(.{ .path = "/abc/123" }, "http://a/abc/123/", "b"); + + try expectAttribute(.{ .path = "/a" }, "http://a/", "b;Path=/a"); + try expectAttribute(.{ .path = "/aa" }, "http://a/", "b;path=/aa;"); + try expectAttribute(.{ .path = "/aabc/" }, "http://a/", "b; path= /aabc/ ;"); + + try expectAttribute(.{ .path = "/bbb/" }, "http://a/", "b; path=/a/; path=/bbb/"); + try expectAttribute(.{ .path = "/cc" }, "http://a/", "b; path=/a/; path=/bbb/; path = /cc"); +} + +test "Cookie: parse secure" { + try expectAttribute(.{ .secure = false }, null, "b"); + try expectAttribute(.{ .secure = false }, null, "b;secured"); + try expectAttribute(.{ .secure = false }, null, "b;security"); + try expectAttribute(.{ .secure = false }, null, "b;SecureX"); + try expectAttribute(.{ .secure = true }, null, "b; Secure"); + try expectAttribute(.{ .secure = true }, null, "b; Secure "); + try expectAttribute(.{ .secure = true }, null, "b; Secure=on "); + try expectAttribute(.{ .secure = true }, null, "b; Secure=Off "); + try expectAttribute(.{ .secure = true }, null, "b; secure=Off "); + try expectAttribute(.{ .secure = true }, null, "b; seCUre=Off "); +} + +test "Cookie: parse httponly" { + try expectAttribute(.{ .http_only = false }, null, "b"); + try expectAttribute(.{ .http_only = false }, null, "b;HttpOnly0"); + try expectAttribute(.{ .http_only = false }, null, "b;H ttpOnly"); + try expectAttribute(.{ .http_only = true }, null, "b; HttpOnly"); + try expectAttribute(.{ .http_only = true }, null, "b; Httponly "); + try expectAttribute(.{ .http_only = true }, null, "b; Httponly=on "); + try expectAttribute(.{ .http_only = true }, null, "b; httpOnly=Off "); + try expectAttribute(.{ .http_only = true }, null, "b; httpOnly=Off "); + try expectAttribute(.{ .http_only = true }, null, "b; HttpOnly=Off "); +} + +test "Cookie: parse strict" { + try expectAttribute(.{ .same_site = .lax }, null, "b;samesite"); + try expectAttribute(.{ .same_site = .lax }, null, "b;samesite=lax"); + try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Lax "); + try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Other "); + try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Nope "); + + try expectAttribute(.{ .same_site = .none }, null, "b; samesite=none "); + try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None "); + try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None;"); + try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None"); + + try expectAttribute(.{ .same_site = .strict }, null, "b; samesite=Strict "); + try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite= STRICT "); + try expectAttribute(.{ .same_site = .strict }, null, "b; SameSITE=strict;"); + try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite=Strict"); + + try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=Strict; SameSite=lax; SameSite=NONE"); +} + +test "Cookie: parse max-age" { + try expectAttribute(.{ .expires = null }, null, "b;max-age"); + try expectAttribute(.{ .expires = null }, null, "b;max-age=abc"); + try expectAttribute(.{ .expires = null }, null, "b;max-age=13.22"); + try expectAttribute(.{ .expires = null }, null, "b;max-age=13abc"); + + try expectAttribute(.{ .expires = std.time.timestamp() + 13 }, null, "b;max-age=13"); + try expectAttribute(.{ .expires = std.time.timestamp() + -22 }, null, "b;max-age=-22"); + try expectAttribute(.{ .expires = std.time.timestamp() + 4294967296 }, null, "b;max-age=4294967296"); + try expectAttribute(.{ .expires = std.time.timestamp() + -4294967296 }, null, "b;Max-Age= -4294967296"); + try expectAttribute(.{ .expires = std.time.timestamp() + 0 }, null, "b; Max-Age=0"); + try expectAttribute(.{ .expires = std.time.timestamp() + 500 }, null, "b; Max-Age = 500 ; Max-Age=invalid"); + try expectAttribute(.{ .expires = std.time.timestamp() + 1000 }, null, "b;max-age=600;max-age=0;max-age = 1000"); +} + +test "Cookie: parse expires" { + try expectAttribute(.{ .expires = null }, null, "b;expires="); + try expectAttribute(.{ .expires = null }, null, "b;expires=abc"); + try expectAttribute(.{ .expires = null }, null, "b;expires=13.22"); + try expectAttribute(.{ .expires = null }, null, "b;expires=33"); + + try expectAttribute(.{ .expires = 1918798080 - std.time.timestamp() }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT"); + // max-age has priority over expires + try expectAttribute(.{ .expires = std.time.timestamp() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT"); +} + +test "Cookie: parse all" { + try expectCookie(.{ + .name = "user-id", + .value = "9000", + .path = "/cms", + .domain = "lightpanda.io", + }, "https://lightpanda.io/cms/users", "user-id=9000"); + + try expectCookie(.{ + .name = "user-id", + .value = "9000", + .path = "/", + .http_only = true, + .secure = true, + .domain = ".lightpanda.io", + .expires = std.time.timestamp() + 30, + }, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io"); +} + +test "Cookie: parse domain" { + try expectAttribute(.{ .domain = "lightpanda.io" }, "http://lightpanda.io/", "b"); + try expectAttribute(.{ .domain = "dev.lightpanda.io" }, "http://dev.lightpanda.io/", "b"); + try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://lightpanda.io/", "b;domain=lightpanda.io"); + try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://lightpanda.io/", "b;domain=.lightpanda.io"); + try expectAttribute(.{ .domain = ".dev.lightpanda.io" }, "http://dev.lightpanda.io/", "b;domain=dev.lightpanda.io"); + try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://dev.lightpanda.io/", "b;domain=lightpanda.io"); + try expectAttribute(.{ .domain = ".lightpanda.io" }, "http://dev.lightpanda.io/", "b;domain=.lightpanda.io"); + + try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=io"); + try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=.io"); + try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.lightpanda.io"); + try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.lightpanda.com"); + try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.example.com"); +} + +const ExpectedCookie = struct { + name: []const u8, + value: []const u8, + path: []const u8, + domain: []const u8, + expires: ?i64 = null, + secure: bool = false, + http_only: bool = false, + same_site: Cookie.SameSite = .lax, +}; + +fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u8) !void { + const uri = try Uri.parse(url); + var cookie = try Cookie.parse(testing.allocator, uri, set_cookie); + defer cookie.deinit(); + + try testing.expectEqual(expected.name, cookie.name); + try testing.expectEqual(expected.value, cookie.value); + try testing.expectEqual(expected.secure, cookie.secure); + try testing.expectEqual(expected.http_only, cookie.http_only); + try testing.expectEqual(expected.same_site, cookie.same_site); + try testing.expectEqual(expected.path, cookie.path); + try testing.expectEqual(expected.domain, cookie.domain); + + try testing.expectDelta(expected.expires, cookie.expires, 2); +} + +fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void { + const uri = if (url) |u| try Uri.parse(u) else dummy_test_uri; + var cookie = try Cookie.parse(testing.allocator, uri, set_cookie); + defer cookie.deinit(); + + inline for (@typeInfo(@TypeOf(expected)).Struct.fields) |f| { + if (comptime std.mem.eql(u8, f.name, "expires")) { + try testing.expectDelta(expected.expires, cookie.expires, 1); + } else { + try testing.expectEqual(@field(expected, f.name), @field(cookie, f.name)); + } + } +} + +fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void { + const uri = if (url) |u| try Uri.parse(u) else dummy_test_uri; + try testing.expectError(expected, Cookie.parse(testing.allocator, uri, set_cookie)); +} + +const dummy_test_uri = Uri.parse("http://lightpanda.io/") catch unreachable; diff --git a/src/testing.zig b/src/testing.zig new file mode 100644 index 000000000..f0b3f6e0c --- /dev/null +++ b/src/testing.zig @@ -0,0 +1,134 @@ +const std = @import("std"); + +pub const allocator = std.testing.allocator; +pub const expectError = std.testing.expectError; +pub const expectString = std.testing.expectEqualStrings; + +// Merged std.testing.expectEqual and std.testing.expectString +// can be useful when testing fields of an anytype an you don't know +// exactly how to assert equality +pub fn expectEqual(expected: anytype, actual: anytype) !void { + switch (@typeInfo(@TypeOf(actual))) { + .Array => |arr| if (arr.child == u8) { + return std.testing.expectEqualStrings(expected, &actual); + }, + .Pointer => |ptr| if (ptr.child == u8) { + return std.testing.expectEqualStrings(expected, actual); + } else if (comptime isStringArray(ptr.child)) { + return std.testing.expectEqualStrings(expected, actual); + } else if (ptr.child == []u8 or ptr.child == []const u8) { + return expectString(expected, actual); + }, + .Struct => |structType| { + inline for (structType.fields) |field| { + try expectEqual(@field(expected, field.name), @field(actual, field.name)); + } + return; + }, + .Optional => { + if (actual == null) { + return std.testing.expectEqual(null, expected); + } + return expectEqual(expected, actual.?); + }, + .Union => |union_info| { + if (union_info.tag_type == null) { + @compileError("Unable to compare untagged union values"); + } + const Tag = std.meta.Tag(@TypeOf(expected)); + + const expectedTag = @as(Tag, expected); + const actualTag = @as(Tag, actual); + try expectEqual(expectedTag, actualTag); + + inline for (std.meta.fields(@TypeOf(actual))) |fld| { + if (std.mem.eql(u8, fld.name, @tagName(actualTag))) { + try expectEqual(@field(expected, fld.name), @field(actual, fld.name)); + return; + } + } + unreachable; + }, + else => {}, + } + return std.testing.expectEqual(expected, actual); +} + +pub fn expectDelta(expected: anytype, actual: anytype, delta: anytype) !void { + if (@typeInfo(@TypeOf(expected)) == .Null) { + return std.testing.expectEqual(null, actual); + } + + switch (@typeInfo(@TypeOf(actual))) { + .Optional => { + if (actual) |value| { + return expectDelta(expected, value, delta); + } + return std.testing.expectEqual(null, expected); + }, + else => {}, + } + + switch (@typeInfo(@TypeOf(expected))) { + .Optional => { + if (expected) |value| { + return expectDelta(value, actual, delta); + } + return std.testing.expectEqual(null, actual); + }, + else => {}, + } + + var diff = expected - actual; + if (diff < 0) { + diff = -diff; + } + if (diff <= delta) { + return; + } + + print("Expected {} to be within {} of {}. Actual diff: {}", .{ expected, delta, actual, diff }); + return error.NotWithinDelta; +} + +fn isStringArray(comptime T: type) bool { + if (!is(.array)(T) and !isPtrTo(.array)(T)) { + return false; + } + return std.meta.Elem(T) == u8; +} + +pub const TraitFn = fn (type) bool; +pub fn is(comptime id: std.builtin.TypeId) TraitFn { + const Closure = struct { + pub fn trait(comptime T: type) bool { + return id == @typeInfo(T); + } + }; + return Closure.trait; +} + +pub fn isPtrTo(comptime id: std.builtin.TypeId) TraitFn { + const Closure = struct { + pub fn trait(comptime T: type) bool { + if (!comptime isSingleItemPtr(T)) return false; + return id == @typeInfo(std.meta.Child(T)); + } + }; + return Closure.trait; +} + +pub fn isSingleItemPtr(comptime T: type) bool { + if (comptime is(.pointer)(T)) { + return @typeInfo(T).Pointer.size == .one; + } + return false; +} + +pub fn print(comptime fmt: []const u8, args: anytype) void { + if (@inComptime()) { + @compileError(std.fmt.comptimePrint(fmt, args)); + } else { + std.debug.print(fmt, args); + } +} diff --git a/src/unit_tests.zig b/src/unit_tests.zig index 0aa70354d..440a1f414 100644 --- a/src/unit_tests.zig +++ b/src/unit_tests.zig @@ -378,8 +378,10 @@ test { std.testing.refAllDecls(@import("generate.zig")); std.testing.refAllDecls(@import("http/Client.zig")); std.testing.refAllDecls(@import("storage/storage.zig")); + std.testing.refAllDecls(@import("storage/cookie.zig")); std.testing.refAllDecls(@import("iterator/iterator.zig")); std.testing.refAllDecls(@import("server.zig")); std.testing.refAllDecls(@import("cdp/cdp.zig")); std.testing.refAllDecls(@import("log.zig")); + std.testing.refAllDecls(@import("datetime.zig")); } From 0a02ae5c7a3fa6fcb16d3fad0f4880e979db8144 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Feb 2025 11:57:46 +0800 Subject: [PATCH 08/13] add test for Storage shed, use map.getOrPut --- src/storage/cookie.zig | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/storage/cookie.zig b/src/storage/cookie.zig index e9a8a30e8..609b2d330 100644 --- a/src/storage/cookie.zig +++ b/src/storage/cookie.zig @@ -5,6 +5,67 @@ const ArenaAllocator = std.heap.ArenaAllocator; const DateTime = @import("../datetime.zig").DateTime; +pub const Jar = struct { + allocator: Allocator, + cookies: std.ArrayListUnmanaged(u8), + + pub fn init(allocator: Allocator) Jar { + return .{ + .cookies = .{}, + .allocator = allocator, + }; + } + + pub fn deinit(self: *Jar) void { + for (self.cookies.items) |c| { + c.deinit(); + } + self.cookies.deinit(self.allocator); + } + + pub fn forRequest( + self: *const Jar, + allocator: Allocator, + request_start: i64, + origin_uri: ?Uri, + target_uri: Uri, + navitation: bool, + ) !CookieList { + const is_secure = std.mem.eql(u8, target_uri.scheme, "https"); + const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded; + const same_site = try areSameSite(origin_uri, target_host); + + var i: usize = 0; + var cookies = self.cookies.items; + while (i < cookies.len) { + const cookie = &cookies[i]; + if (cookie.isExpired(request_start)) { + self.swapRemove(i); + // don't increment i ! + continue; + } + i += 1; + + if (is_secure == false and cookie.secure) { + continue; + } + + // www.google.com + + if (navitation == false and cookie.same_site != .strict) { + continue; + } + } + } +}; + +// abc.lightpanda.io is the same site as lightpanda.io or 123.lightpanda.io +// or spice.123.lightpanda.io +fn areSameSite(origin_uri_: ?std.Uri, target_host: []const u8) !bool { + const origin_uri = origin_uri_ orelse return true; + const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded; +} + pub const Cookie = struct { arena: ArenaAllocator, name: []const u8, From b091c01ede924017294dd523be4fd1d5ea9a4339 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Feb 2025 16:09:10 +0800 Subject: [PATCH 09/13] add cookie jar --- Makefile | 5 +- src/data/public_suffix_list.zig | 9742 ++++++++++++++++++++++++++++ src/data/public_suffix_list_gen.go | 42 + src/storage/cookie.zig | 365 +- 4 files changed, 10139 insertions(+), 15 deletions(-) create mode 100644 src/data/public_suffix_list.zig create mode 100644 src/data/public_suffix_list_gen.go diff --git a/Makefile b/Makefile index 042f356e2..7f099b056 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ help: # $(ZIG) commands # ------------ -.PHONY: build build-dev run run-release shell test bench download-zig wpt unittest +.PHONY: build build-dev run run-release shell test bench download-zig wpt unittest data zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2) @@ -199,6 +199,9 @@ install-zig-js-runtime: @cd vendor/zig-js-runtime && \ make install +data: + cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig + .PHONY: _build_mimalloc MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH) diff --git a/src/data/public_suffix_list.zig b/src/data/public_suffix_list.zig new file mode 100644 index 000000000..76107c9eb --- /dev/null +++ b/src/data/public_suffix_list.zig @@ -0,0 +1,9742 @@ +const std = @import("std"); + +pub fn lookup(value: []const u8) bool { + return public_suffix_list.has(value); +} + +const public_suffix_list = std.StaticStringMap(void).initComptime([_]struct { []const u8, void }{ + .{ "ac", {} }, + .{ "com.ac", {} }, + .{ "edu.ac", {} }, + .{ "gov.ac", {} }, + .{ "mil.ac", {} }, + .{ "net.ac", {} }, + .{ "org.ac", {} }, + .{ "ad", {} }, + .{ "ae", {} }, + .{ "ac.ae", {} }, + .{ "co.ae", {} }, + .{ "gov.ae", {} }, + .{ "mil.ae", {} }, + .{ "net.ae", {} }, + .{ "org.ae", {} }, + .{ "sch.ae", {} }, + .{ "aero", {} }, + .{ "airline.aero", {} }, + .{ "airport.aero", {} }, + .{ "accident-investigation.aero", {} }, + .{ "accident-prevention.aero", {} }, + .{ "aerobatic.aero", {} }, + .{ "aeroclub.aero", {} }, + .{ "aerodrome.aero", {} }, + .{ "agents.aero", {} }, + .{ "air-surveillance.aero", {} }, + .{ "air-traffic-control.aero", {} }, + .{ "aircraft.aero", {} }, + .{ "airtraffic.aero", {} }, + .{ "ambulance.aero", {} }, + .{ "association.aero", {} }, + .{ "author.aero", {} }, + .{ "ballooning.aero", {} }, + .{ "broker.aero", {} }, + .{ "caa.aero", {} }, + .{ "cargo.aero", {} }, + .{ "catering.aero", {} }, + .{ "certification.aero", {} }, + .{ "championship.aero", {} }, + .{ "charter.aero", {} }, + .{ "civilaviation.aero", {} }, + .{ "club.aero", {} }, + .{ "conference.aero", {} }, + .{ "consultant.aero", {} }, + .{ "consulting.aero", {} }, + .{ "control.aero", {} }, + .{ "council.aero", {} }, + .{ "crew.aero", {} }, + .{ "design.aero", {} }, + .{ "dgca.aero", {} }, + .{ "educator.aero", {} }, + .{ "emergency.aero", {} }, + .{ "engine.aero", {} }, + .{ "engineer.aero", {} }, + .{ "entertainment.aero", {} }, + .{ "equipment.aero", {} }, + .{ "exchange.aero", {} }, + .{ "express.aero", {} }, + .{ "federation.aero", {} }, + .{ "flight.aero", {} }, + .{ "freight.aero", {} }, + .{ "fuel.aero", {} }, + .{ "gliding.aero", {} }, + .{ "government.aero", {} }, + .{ "groundhandling.aero", {} }, + .{ "group.aero", {} }, + .{ "hanggliding.aero", {} }, + .{ "homebuilt.aero", {} }, + .{ "insurance.aero", {} }, + .{ "journal.aero", {} }, + .{ "journalist.aero", {} }, + .{ "leasing.aero", {} }, + .{ "logistics.aero", {} }, + .{ "magazine.aero", {} }, + .{ "maintenance.aero", {} }, + .{ "marketplace.aero", {} }, + .{ "media.aero", {} }, + .{ "microlight.aero", {} }, + .{ "modelling.aero", {} }, + .{ "navigation.aero", {} }, + .{ "parachuting.aero", {} }, + .{ "paragliding.aero", {} }, + .{ "passenger-association.aero", {} }, + .{ "pilot.aero", {} }, + .{ "press.aero", {} }, + .{ "production.aero", {} }, + .{ "recreation.aero", {} }, + .{ "repbody.aero", {} }, + .{ "res.aero", {} }, + .{ "research.aero", {} }, + .{ "rotorcraft.aero", {} }, + .{ "safety.aero", {} }, + .{ "scientist.aero", {} }, + .{ "services.aero", {} }, + .{ "show.aero", {} }, + .{ "skydiving.aero", {} }, + .{ "software.aero", {} }, + .{ "student.aero", {} }, + .{ "taxi.aero", {} }, + .{ "trader.aero", {} }, + .{ "trading.aero", {} }, + .{ "trainer.aero", {} }, + .{ "union.aero", {} }, + .{ "workinggroup.aero", {} }, + .{ "works.aero", {} }, + .{ "af", {} }, + .{ "com.af", {} }, + .{ "edu.af", {} }, + .{ "gov.af", {} }, + .{ "net.af", {} }, + .{ "org.af", {} }, + .{ "ag", {} }, + .{ "co.ag", {} }, + .{ "com.ag", {} }, + .{ "net.ag", {} }, + .{ "nom.ag", {} }, + .{ "org.ag", {} }, + .{ "ai", {} }, + .{ "com.ai", {} }, + .{ "net.ai", {} }, + .{ "off.ai", {} }, + .{ "org.ai", {} }, + .{ "al", {} }, + .{ "com.al", {} }, + .{ "edu.al", {} }, + .{ "gov.al", {} }, + .{ "mil.al", {} }, + .{ "net.al", {} }, + .{ "org.al", {} }, + .{ "am", {} }, + .{ "co.am", {} }, + .{ "com.am", {} }, + .{ "commune.am", {} }, + .{ "net.am", {} }, + .{ "org.am", {} }, + .{ "ao", {} }, + .{ "co.ao", {} }, + .{ "ed.ao", {} }, + .{ "edu.ao", {} }, + .{ "gov.ao", {} }, + .{ "gv.ao", {} }, + .{ "it.ao", {} }, + .{ "og.ao", {} }, + .{ "org.ao", {} }, + .{ "pb.ao", {} }, + .{ "aq", {} }, + .{ "ar", {} }, + .{ "bet.ar", {} }, + .{ "com.ar", {} }, + .{ "coop.ar", {} }, + .{ "edu.ar", {} }, + .{ "gob.ar", {} }, + .{ "gov.ar", {} }, + .{ "int.ar", {} }, + .{ "mil.ar", {} }, + .{ "musica.ar", {} }, + .{ "mutual.ar", {} }, + .{ "net.ar", {} }, + .{ "org.ar", {} }, + .{ "senasa.ar", {} }, + .{ "tur.ar", {} }, + .{ "arpa", {} }, + .{ "e164.arpa", {} }, + .{ "home.arpa", {} }, + .{ "in-addr.arpa", {} }, + .{ "ip6.arpa", {} }, + .{ "iris.arpa", {} }, + .{ "uri.arpa", {} }, + .{ "urn.arpa", {} }, + .{ "as", {} }, + .{ "gov.as", {} }, + .{ "asia", {} }, + .{ "at", {} }, + .{ "ac.at", {} }, + .{ "sth.ac.at", {} }, + .{ "co.at", {} }, + .{ "gv.at", {} }, + .{ "or.at", {} }, + .{ "au", {} }, + .{ "asn.au", {} }, + .{ "com.au", {} }, + .{ "edu.au", {} }, + .{ "gov.au", {} }, + .{ "id.au", {} }, + .{ "net.au", {} }, + .{ "org.au", {} }, + .{ "conf.au", {} }, + .{ "oz.au", {} }, + .{ "act.au", {} }, + .{ "nsw.au", {} }, + .{ "nt.au", {} }, + .{ "qld.au", {} }, + .{ "sa.au", {} }, + .{ "tas.au", {} }, + .{ "vic.au", {} }, + .{ "wa.au", {} }, + .{ "act.edu.au", {} }, + .{ "catholic.edu.au", {} }, + .{ "nsw.edu.au", {} }, + .{ "nt.edu.au", {} }, + .{ "qld.edu.au", {} }, + .{ "sa.edu.au", {} }, + .{ "tas.edu.au", {} }, + .{ "vic.edu.au", {} }, + .{ "wa.edu.au", {} }, + .{ "qld.gov.au", {} }, + .{ "sa.gov.au", {} }, + .{ "tas.gov.au", {} }, + .{ "vic.gov.au", {} }, + .{ "wa.gov.au", {} }, + .{ "schools.nsw.edu.au", {} }, + .{ "aw", {} }, + .{ "com.aw", {} }, + .{ "ax", {} }, + .{ "az", {} }, + .{ "biz.az", {} }, + .{ "co.az", {} }, + .{ "com.az", {} }, + .{ "edu.az", {} }, + .{ "gov.az", {} }, + .{ "info.az", {} }, + .{ "int.az", {} }, + .{ "mil.az", {} }, + .{ "name.az", {} }, + .{ "net.az", {} }, + .{ "org.az", {} }, + .{ "pp.az", {} }, + .{ "pro.az", {} }, + .{ "ba", {} }, + .{ "com.ba", {} }, + .{ "edu.ba", {} }, + .{ "gov.ba", {} }, + .{ "mil.ba", {} }, + .{ "net.ba", {} }, + .{ "org.ba", {} }, + .{ "bb", {} }, + .{ "biz.bb", {} }, + .{ "co.bb", {} }, + .{ "com.bb", {} }, + .{ "edu.bb", {} }, + .{ "gov.bb", {} }, + .{ "info.bb", {} }, + .{ "net.bb", {} }, + .{ "org.bb", {} }, + .{ "store.bb", {} }, + .{ "tv.bb", {} }, + .{ "*.bd", {} }, + .{ "be", {} }, + .{ "ac.be", {} }, + .{ "bf", {} }, + .{ "gov.bf", {} }, + .{ "bg", {} }, + .{ "0.bg", {} }, + .{ "1.bg", {} }, + .{ "2.bg", {} }, + .{ "3.bg", {} }, + .{ "4.bg", {} }, + .{ "5.bg", {} }, + .{ "6.bg", {} }, + .{ "7.bg", {} }, + .{ "8.bg", {} }, + .{ "9.bg", {} }, + .{ "a.bg", {} }, + .{ "b.bg", {} }, + .{ "c.bg", {} }, + .{ "d.bg", {} }, + .{ "e.bg", {} }, + .{ "f.bg", {} }, + .{ "g.bg", {} }, + .{ "h.bg", {} }, + .{ "i.bg", {} }, + .{ "j.bg", {} }, + .{ "k.bg", {} }, + .{ "l.bg", {} }, + .{ "m.bg", {} }, + .{ "n.bg", {} }, + .{ "o.bg", {} }, + .{ "p.bg", {} }, + .{ "q.bg", {} }, + .{ "r.bg", {} }, + .{ "s.bg", {} }, + .{ "t.bg", {} }, + .{ "u.bg", {} }, + .{ "v.bg", {} }, + .{ "w.bg", {} }, + .{ "x.bg", {} }, + .{ "y.bg", {} }, + .{ "z.bg", {} }, + .{ "bh", {} }, + .{ "com.bh", {} }, + .{ "edu.bh", {} }, + .{ "gov.bh", {} }, + .{ "net.bh", {} }, + .{ "org.bh", {} }, + .{ "bi", {} }, + .{ "co.bi", {} }, + .{ "com.bi", {} }, + .{ "edu.bi", {} }, + .{ "or.bi", {} }, + .{ "org.bi", {} }, + .{ "biz", {} }, + .{ "bj", {} }, + .{ "africa.bj", {} }, + .{ "agro.bj", {} }, + .{ "architectes.bj", {} }, + .{ "assur.bj", {} }, + .{ "avocats.bj", {} }, + .{ "co.bj", {} }, + .{ "com.bj", {} }, + .{ "eco.bj", {} }, + .{ "econo.bj", {} }, + .{ "edu.bj", {} }, + .{ "info.bj", {} }, + .{ "loisirs.bj", {} }, + .{ "money.bj", {} }, + .{ "net.bj", {} }, + .{ "org.bj", {} }, + .{ "ote.bj", {} }, + .{ "restaurant.bj", {} }, + .{ "resto.bj", {} }, + .{ "tourism.bj", {} }, + .{ "univ.bj", {} }, + .{ "bm", {} }, + .{ "com.bm", {} }, + .{ "edu.bm", {} }, + .{ "gov.bm", {} }, + .{ "net.bm", {} }, + .{ "org.bm", {} }, + .{ "bn", {} }, + .{ "com.bn", {} }, + .{ "edu.bn", {} }, + .{ "gov.bn", {} }, + .{ "net.bn", {} }, + .{ "org.bn", {} }, + .{ "bo", {} }, + .{ "com.bo", {} }, + .{ "edu.bo", {} }, + .{ "gob.bo", {} }, + .{ "int.bo", {} }, + .{ "mil.bo", {} }, + .{ "net.bo", {} }, + .{ "org.bo", {} }, + .{ "tv.bo", {} }, + .{ "web.bo", {} }, + .{ "academia.bo", {} }, + .{ "agro.bo", {} }, + .{ "arte.bo", {} }, + .{ "blog.bo", {} }, + .{ "bolivia.bo", {} }, + .{ "ciencia.bo", {} }, + .{ "cooperativa.bo", {} }, + .{ "democracia.bo", {} }, + .{ "deporte.bo", {} }, + .{ "ecologia.bo", {} }, + .{ "economia.bo", {} }, + .{ "empresa.bo", {} }, + .{ "indigena.bo", {} }, + .{ "industria.bo", {} }, + .{ "info.bo", {} }, + .{ "medicina.bo", {} }, + .{ "movimiento.bo", {} }, + .{ "musica.bo", {} }, + .{ "natural.bo", {} }, + .{ "nombre.bo", {} }, + .{ "noticias.bo", {} }, + .{ "patria.bo", {} }, + .{ "plurinacional.bo", {} }, + .{ "politica.bo", {} }, + .{ "profesional.bo", {} }, + .{ "pueblo.bo", {} }, + .{ "revista.bo", {} }, + .{ "salud.bo", {} }, + .{ "tecnologia.bo", {} }, + .{ "tksat.bo", {} }, + .{ "transporte.bo", {} }, + .{ "wiki.bo", {} }, + .{ "br", {} }, + .{ "9guacu.br", {} }, + .{ "abc.br", {} }, + .{ "adm.br", {} }, + .{ "adv.br", {} }, + .{ "agr.br", {} }, + .{ "aju.br", {} }, + .{ "am.br", {} }, + .{ "anani.br", {} }, + .{ "aparecida.br", {} }, + .{ "app.br", {} }, + .{ "arq.br", {} }, + .{ "art.br", {} }, + .{ "ato.br", {} }, + .{ "b.br", {} }, + .{ "barueri.br", {} }, + .{ "belem.br", {} }, + .{ "bet.br", {} }, + .{ "bhz.br", {} }, + .{ "bib.br", {} }, + .{ "bio.br", {} }, + .{ "blog.br", {} }, + .{ "bmd.br", {} }, + .{ "boavista.br", {} }, + .{ "bsb.br", {} }, + .{ "campinagrande.br", {} }, + .{ "campinas.br", {} }, + .{ "caxias.br", {} }, + .{ "cim.br", {} }, + .{ "cng.br", {} }, + .{ "cnt.br", {} }, + .{ "com.br", {} }, + .{ "contagem.br", {} }, + .{ "coop.br", {} }, + .{ "coz.br", {} }, + .{ "cri.br", {} }, + .{ "cuiaba.br", {} }, + .{ "curitiba.br", {} }, + .{ "def.br", {} }, + .{ "des.br", {} }, + .{ "det.br", {} }, + .{ "dev.br", {} }, + .{ "ecn.br", {} }, + .{ "eco.br", {} }, + .{ "edu.br", {} }, + .{ "emp.br", {} }, + .{ "enf.br", {} }, + .{ "eng.br", {} }, + .{ "esp.br", {} }, + .{ "etc.br", {} }, + .{ "eti.br", {} }, + .{ "far.br", {} }, + .{ "feira.br", {} }, + .{ "flog.br", {} }, + .{ "floripa.br", {} }, + .{ "fm.br", {} }, + .{ "fnd.br", {} }, + .{ "fortal.br", {} }, + .{ "fot.br", {} }, + .{ "foz.br", {} }, + .{ "fst.br", {} }, + .{ "g12.br", {} }, + .{ "geo.br", {} }, + .{ "ggf.br", {} }, + .{ "goiania.br", {} }, + .{ "gov.br", {} }, + .{ "ac.gov.br", {} }, + .{ "al.gov.br", {} }, + .{ "am.gov.br", {} }, + .{ "ap.gov.br", {} }, + .{ "ba.gov.br", {} }, + .{ "ce.gov.br", {} }, + .{ "df.gov.br", {} }, + .{ "es.gov.br", {} }, + .{ "go.gov.br", {} }, + .{ "ma.gov.br", {} }, + .{ "mg.gov.br", {} }, + .{ "ms.gov.br", {} }, + .{ "mt.gov.br", {} }, + .{ "pa.gov.br", {} }, + .{ "pb.gov.br", {} }, + .{ "pe.gov.br", {} }, + .{ "pi.gov.br", {} }, + .{ "pr.gov.br", {} }, + .{ "rj.gov.br", {} }, + .{ "rn.gov.br", {} }, + .{ "ro.gov.br", {} }, + .{ "rr.gov.br", {} }, + .{ "rs.gov.br", {} }, + .{ "sc.gov.br", {} }, + .{ "se.gov.br", {} }, + .{ "sp.gov.br", {} }, + .{ "to.gov.br", {} }, + .{ "gru.br", {} }, + .{ "imb.br", {} }, + .{ "ind.br", {} }, + .{ "inf.br", {} }, + .{ "jab.br", {} }, + .{ "jampa.br", {} }, + .{ "jdf.br", {} }, + .{ "joinville.br", {} }, + .{ "jor.br", {} }, + .{ "jus.br", {} }, + .{ "leg.br", {} }, + .{ "leilao.br", {} }, + .{ "lel.br", {} }, + .{ "log.br", {} }, + .{ "londrina.br", {} }, + .{ "macapa.br", {} }, + .{ "maceio.br", {} }, + .{ "manaus.br", {} }, + .{ "maringa.br", {} }, + .{ "mat.br", {} }, + .{ "med.br", {} }, + .{ "mil.br", {} }, + .{ "morena.br", {} }, + .{ "mp.br", {} }, + .{ "mus.br", {} }, + .{ "natal.br", {} }, + .{ "net.br", {} }, + .{ "niteroi.br", {} }, + .{ "*.nom.br", {} }, + .{ "not.br", {} }, + .{ "ntr.br", {} }, + .{ "odo.br", {} }, + .{ "ong.br", {} }, + .{ "org.br", {} }, + .{ "osasco.br", {} }, + .{ "palmas.br", {} }, + .{ "poa.br", {} }, + .{ "ppg.br", {} }, + .{ "pro.br", {} }, + .{ "psc.br", {} }, + .{ "psi.br", {} }, + .{ "pvh.br", {} }, + .{ "qsl.br", {} }, + .{ "radio.br", {} }, + .{ "rec.br", {} }, + .{ "recife.br", {} }, + .{ "rep.br", {} }, + .{ "ribeirao.br", {} }, + .{ "rio.br", {} }, + .{ "riobranco.br", {} }, + .{ "riopreto.br", {} }, + .{ "salvador.br", {} }, + .{ "sampa.br", {} }, + .{ "santamaria.br", {} }, + .{ "santoandre.br", {} }, + .{ "saobernardo.br", {} }, + .{ "saogonca.br", {} }, + .{ "seg.br", {} }, + .{ "sjc.br", {} }, + .{ "slg.br", {} }, + .{ "slz.br", {} }, + .{ "sorocaba.br", {} }, + .{ "srv.br", {} }, + .{ "taxi.br", {} }, + .{ "tc.br", {} }, + .{ "tec.br", {} }, + .{ "teo.br", {} }, + .{ "the.br", {} }, + .{ "tmp.br", {} }, + .{ "trd.br", {} }, + .{ "tur.br", {} }, + .{ "tv.br", {} }, + .{ "udi.br", {} }, + .{ "vet.br", {} }, + .{ "vix.br", {} }, + .{ "vlog.br", {} }, + .{ "wiki.br", {} }, + .{ "zlg.br", {} }, + .{ "bs", {} }, + .{ "com.bs", {} }, + .{ "edu.bs", {} }, + .{ "gov.bs", {} }, + .{ "net.bs", {} }, + .{ "org.bs", {} }, + .{ "bt", {} }, + .{ "com.bt", {} }, + .{ "edu.bt", {} }, + .{ "gov.bt", {} }, + .{ "net.bt", {} }, + .{ "org.bt", {} }, + .{ "bv", {} }, + .{ "bw", {} }, + .{ "ac.bw", {} }, + .{ "co.bw", {} }, + .{ "gov.bw", {} }, + .{ "net.bw", {} }, + .{ "org.bw", {} }, + .{ "by", {} }, + .{ "gov.by", {} }, + .{ "mil.by", {} }, + .{ "com.by", {} }, + .{ "of.by", {} }, + .{ "bz", {} }, + .{ "co.bz", {} }, + .{ "com.bz", {} }, + .{ "edu.bz", {} }, + .{ "gov.bz", {} }, + .{ "net.bz", {} }, + .{ "org.bz", {} }, + .{ "ca", {} }, + .{ "ab.ca", {} }, + .{ "bc.ca", {} }, + .{ "mb.ca", {} }, + .{ "nb.ca", {} }, + .{ "nf.ca", {} }, + .{ "nl.ca", {} }, + .{ "ns.ca", {} }, + .{ "nt.ca", {} }, + .{ "nu.ca", {} }, + .{ "on.ca", {} }, + .{ "pe.ca", {} }, + .{ "qc.ca", {} }, + .{ "sk.ca", {} }, + .{ "yk.ca", {} }, + .{ "gc.ca", {} }, + .{ "cat", {} }, + .{ "cc", {} }, + .{ "cd", {} }, + .{ "gov.cd", {} }, + .{ "cf", {} }, + .{ "cg", {} }, + .{ "ch", {} }, + .{ "ci", {} }, + .{ "ac.ci", {} }, + .{ "aéroport.ci", {} }, + .{ "asso.ci", {} }, + .{ "co.ci", {} }, + .{ "com.ci", {} }, + .{ "ed.ci", {} }, + .{ "edu.ci", {} }, + .{ "go.ci", {} }, + .{ "gouv.ci", {} }, + .{ "int.ci", {} }, + .{ "net.ci", {} }, + .{ "or.ci", {} }, + .{ "org.ci", {} }, + .{ "*.ck", {} }, + .{ "!www.ck", {} }, + .{ "cl", {} }, + .{ "co.cl", {} }, + .{ "gob.cl", {} }, + .{ "gov.cl", {} }, + .{ "mil.cl", {} }, + .{ "cm", {} }, + .{ "co.cm", {} }, + .{ "com.cm", {} }, + .{ "gov.cm", {} }, + .{ "net.cm", {} }, + .{ "cn", {} }, + .{ "ac.cn", {} }, + .{ "com.cn", {} }, + .{ "edu.cn", {} }, + .{ "gov.cn", {} }, + .{ "mil.cn", {} }, + .{ "net.cn", {} }, + .{ "org.cn", {} }, + .{ "公司.cn", {} }, + .{ "網絡.cn", {} }, + .{ "网络.cn", {} }, + .{ "ah.cn", {} }, + .{ "bj.cn", {} }, + .{ "cq.cn", {} }, + .{ "fj.cn", {} }, + .{ "gd.cn", {} }, + .{ "gs.cn", {} }, + .{ "gx.cn", {} }, + .{ "gz.cn", {} }, + .{ "ha.cn", {} }, + .{ "hb.cn", {} }, + .{ "he.cn", {} }, + .{ "hi.cn", {} }, + .{ "hk.cn", {} }, + .{ "hl.cn", {} }, + .{ "hn.cn", {} }, + .{ "jl.cn", {} }, + .{ "js.cn", {} }, + .{ "jx.cn", {} }, + .{ "ln.cn", {} }, + .{ "mo.cn", {} }, + .{ "nm.cn", {} }, + .{ "nx.cn", {} }, + .{ "qh.cn", {} }, + .{ "sc.cn", {} }, + .{ "sd.cn", {} }, + .{ "sh.cn", {} }, + .{ "sn.cn", {} }, + .{ "sx.cn", {} }, + .{ "tj.cn", {} }, + .{ "tw.cn", {} }, + .{ "xj.cn", {} }, + .{ "xz.cn", {} }, + .{ "yn.cn", {} }, + .{ "zj.cn", {} }, + .{ "co", {} }, + .{ "com.co", {} }, + .{ "edu.co", {} }, + .{ "gov.co", {} }, + .{ "mil.co", {} }, + .{ "net.co", {} }, + .{ "nom.co", {} }, + .{ "org.co", {} }, + .{ "com", {} }, + .{ "coop", {} }, + .{ "cr", {} }, + .{ "ac.cr", {} }, + .{ "co.cr", {} }, + .{ "ed.cr", {} }, + .{ "fi.cr", {} }, + .{ "go.cr", {} }, + .{ "or.cr", {} }, + .{ "sa.cr", {} }, + .{ "cu", {} }, + .{ "com.cu", {} }, + .{ "edu.cu", {} }, + .{ "gob.cu", {} }, + .{ "inf.cu", {} }, + .{ "nat.cu", {} }, + .{ "net.cu", {} }, + .{ "org.cu", {} }, + .{ "cv", {} }, + .{ "com.cv", {} }, + .{ "edu.cv", {} }, + .{ "id.cv", {} }, + .{ "int.cv", {} }, + .{ "net.cv", {} }, + .{ "nome.cv", {} }, + .{ "org.cv", {} }, + .{ "publ.cv", {} }, + .{ "cw", {} }, + .{ "com.cw", {} }, + .{ "edu.cw", {} }, + .{ "net.cw", {} }, + .{ "org.cw", {} }, + .{ "cx", {} }, + .{ "gov.cx", {} }, + .{ "cy", {} }, + .{ "ac.cy", {} }, + .{ "biz.cy", {} }, + .{ "com.cy", {} }, + .{ "ekloges.cy", {} }, + .{ "gov.cy", {} }, + .{ "ltd.cy", {} }, + .{ "mil.cy", {} }, + .{ "net.cy", {} }, + .{ "org.cy", {} }, + .{ "press.cy", {} }, + .{ "pro.cy", {} }, + .{ "tm.cy", {} }, + .{ "cz", {} }, + .{ "de", {} }, + .{ "dj", {} }, + .{ "dk", {} }, + .{ "dm", {} }, + .{ "co.dm", {} }, + .{ "com.dm", {} }, + .{ "edu.dm", {} }, + .{ "gov.dm", {} }, + .{ "net.dm", {} }, + .{ "org.dm", {} }, + .{ "do", {} }, + .{ "art.do", {} }, + .{ "com.do", {} }, + .{ "edu.do", {} }, + .{ "gob.do", {} }, + .{ "gov.do", {} }, + .{ "mil.do", {} }, + .{ "net.do", {} }, + .{ "org.do", {} }, + .{ "sld.do", {} }, + .{ "web.do", {} }, + .{ "dz", {} }, + .{ "art.dz", {} }, + .{ "asso.dz", {} }, + .{ "com.dz", {} }, + .{ "edu.dz", {} }, + .{ "gov.dz", {} }, + .{ "net.dz", {} }, + .{ "org.dz", {} }, + .{ "pol.dz", {} }, + .{ "soc.dz", {} }, + .{ "tm.dz", {} }, + .{ "ec", {} }, + .{ "com.ec", {} }, + .{ "edu.ec", {} }, + .{ "fin.ec", {} }, + .{ "gob.ec", {} }, + .{ "gov.ec", {} }, + .{ "info.ec", {} }, + .{ "k12.ec", {} }, + .{ "med.ec", {} }, + .{ "mil.ec", {} }, + .{ "net.ec", {} }, + .{ "org.ec", {} }, + .{ "pro.ec", {} }, + .{ "edu", {} }, + .{ "ee", {} }, + .{ "aip.ee", {} }, + .{ "com.ee", {} }, + .{ "edu.ee", {} }, + .{ "fie.ee", {} }, + .{ "gov.ee", {} }, + .{ "lib.ee", {} }, + .{ "med.ee", {} }, + .{ "org.ee", {} }, + .{ "pri.ee", {} }, + .{ "riik.ee", {} }, + .{ "eg", {} }, + .{ "ac.eg", {} }, + .{ "com.eg", {} }, + .{ "edu.eg", {} }, + .{ "eun.eg", {} }, + .{ "gov.eg", {} }, + .{ "info.eg", {} }, + .{ "me.eg", {} }, + .{ "mil.eg", {} }, + .{ "name.eg", {} }, + .{ "net.eg", {} }, + .{ "org.eg", {} }, + .{ "sci.eg", {} }, + .{ "sport.eg", {} }, + .{ "tv.eg", {} }, + .{ "*.er", {} }, + .{ "es", {} }, + .{ "com.es", {} }, + .{ "edu.es", {} }, + .{ "gob.es", {} }, + .{ "nom.es", {} }, + .{ "org.es", {} }, + .{ "et", {} }, + .{ "biz.et", {} }, + .{ "com.et", {} }, + .{ "edu.et", {} }, + .{ "gov.et", {} }, + .{ "info.et", {} }, + .{ "name.et", {} }, + .{ "net.et", {} }, + .{ "org.et", {} }, + .{ "eu", {} }, + .{ "fi", {} }, + .{ "aland.fi", {} }, + .{ "fj", {} }, + .{ "ac.fj", {} }, + .{ "biz.fj", {} }, + .{ "com.fj", {} }, + .{ "gov.fj", {} }, + .{ "info.fj", {} }, + .{ "mil.fj", {} }, + .{ "name.fj", {} }, + .{ "net.fj", {} }, + .{ "org.fj", {} }, + .{ "pro.fj", {} }, + .{ "*.fk", {} }, + .{ "fm", {} }, + .{ "com.fm", {} }, + .{ "edu.fm", {} }, + .{ "net.fm", {} }, + .{ "org.fm", {} }, + .{ "fo", {} }, + .{ "fr", {} }, + .{ "asso.fr", {} }, + .{ "com.fr", {} }, + .{ "gouv.fr", {} }, + .{ "nom.fr", {} }, + .{ "prd.fr", {} }, + .{ "tm.fr", {} }, + .{ "avoues.fr", {} }, + .{ "cci.fr", {} }, + .{ "greta.fr", {} }, + .{ "huissier-justice.fr", {} }, + .{ "ga", {} }, + .{ "gb", {} }, + .{ "gd", {} }, + .{ "edu.gd", {} }, + .{ "gov.gd", {} }, + .{ "ge", {} }, + .{ "com.ge", {} }, + .{ "edu.ge", {} }, + .{ "gov.ge", {} }, + .{ "net.ge", {} }, + .{ "org.ge", {} }, + .{ "pvt.ge", {} }, + .{ "school.ge", {} }, + .{ "gf", {} }, + .{ "gg", {} }, + .{ "co.gg", {} }, + .{ "net.gg", {} }, + .{ "org.gg", {} }, + .{ "gh", {} }, + .{ "com.gh", {} }, + .{ "edu.gh", {} }, + .{ "gov.gh", {} }, + .{ "mil.gh", {} }, + .{ "org.gh", {} }, + .{ "gi", {} }, + .{ "com.gi", {} }, + .{ "edu.gi", {} }, + .{ "gov.gi", {} }, + .{ "ltd.gi", {} }, + .{ "mod.gi", {} }, + .{ "org.gi", {} }, + .{ "gl", {} }, + .{ "co.gl", {} }, + .{ "com.gl", {} }, + .{ "edu.gl", {} }, + .{ "net.gl", {} }, + .{ "org.gl", {} }, + .{ "gm", {} }, + .{ "gn", {} }, + .{ "ac.gn", {} }, + .{ "com.gn", {} }, + .{ "edu.gn", {} }, + .{ "gov.gn", {} }, + .{ "net.gn", {} }, + .{ "org.gn", {} }, + .{ "gov", {} }, + .{ "gp", {} }, + .{ "asso.gp", {} }, + .{ "com.gp", {} }, + .{ "edu.gp", {} }, + .{ "mobi.gp", {} }, + .{ "net.gp", {} }, + .{ "org.gp", {} }, + .{ "gq", {} }, + .{ "gr", {} }, + .{ "com.gr", {} }, + .{ "edu.gr", {} }, + .{ "gov.gr", {} }, + .{ "net.gr", {} }, + .{ "org.gr", {} }, + .{ "gs", {} }, + .{ "gt", {} }, + .{ "com.gt", {} }, + .{ "edu.gt", {} }, + .{ "gob.gt", {} }, + .{ "ind.gt", {} }, + .{ "mil.gt", {} }, + .{ "net.gt", {} }, + .{ "org.gt", {} }, + .{ "gu", {} }, + .{ "com.gu", {} }, + .{ "edu.gu", {} }, + .{ "gov.gu", {} }, + .{ "guam.gu", {} }, + .{ "info.gu", {} }, + .{ "net.gu", {} }, + .{ "org.gu", {} }, + .{ "web.gu", {} }, + .{ "gw", {} }, + .{ "gy", {} }, + .{ "co.gy", {} }, + .{ "com.gy", {} }, + .{ "edu.gy", {} }, + .{ "gov.gy", {} }, + .{ "net.gy", {} }, + .{ "org.gy", {} }, + .{ "hk", {} }, + .{ "com.hk", {} }, + .{ "edu.hk", {} }, + .{ "gov.hk", {} }, + .{ "idv.hk", {} }, + .{ "net.hk", {} }, + .{ "org.hk", {} }, + .{ "个人.hk", {} }, + .{ "個人.hk", {} }, + .{ "公司.hk", {} }, + .{ "政府.hk", {} }, + .{ "敎育.hk", {} }, + .{ "教育.hk", {} }, + .{ "箇人.hk", {} }, + .{ "組織.hk", {} }, + .{ "組织.hk", {} }, + .{ "網絡.hk", {} }, + .{ "網络.hk", {} }, + .{ "组織.hk", {} }, + .{ "组织.hk", {} }, + .{ "网絡.hk", {} }, + .{ "网络.hk", {} }, + .{ "hm", {} }, + .{ "hn", {} }, + .{ "com.hn", {} }, + .{ "edu.hn", {} }, + .{ "gob.hn", {} }, + .{ "mil.hn", {} }, + .{ "net.hn", {} }, + .{ "org.hn", {} }, + .{ "hr", {} }, + .{ "com.hr", {} }, + .{ "from.hr", {} }, + .{ "iz.hr", {} }, + .{ "name.hr", {} }, + .{ "ht", {} }, + .{ "adult.ht", {} }, + .{ "art.ht", {} }, + .{ "asso.ht", {} }, + .{ "com.ht", {} }, + .{ "coop.ht", {} }, + .{ "edu.ht", {} }, + .{ "firm.ht", {} }, + .{ "gouv.ht", {} }, + .{ "info.ht", {} }, + .{ "med.ht", {} }, + .{ "net.ht", {} }, + .{ "org.ht", {} }, + .{ "perso.ht", {} }, + .{ "pol.ht", {} }, + .{ "pro.ht", {} }, + .{ "rel.ht", {} }, + .{ "shop.ht", {} }, + .{ "hu", {} }, + .{ "2000.hu", {} }, + .{ "agrar.hu", {} }, + .{ "bolt.hu", {} }, + .{ "casino.hu", {} }, + .{ "city.hu", {} }, + .{ "co.hu", {} }, + .{ "erotica.hu", {} }, + .{ "erotika.hu", {} }, + .{ "film.hu", {} }, + .{ "forum.hu", {} }, + .{ "games.hu", {} }, + .{ "hotel.hu", {} }, + .{ "info.hu", {} }, + .{ "ingatlan.hu", {} }, + .{ "jogasz.hu", {} }, + .{ "konyvelo.hu", {} }, + .{ "lakas.hu", {} }, + .{ "media.hu", {} }, + .{ "news.hu", {} }, + .{ "org.hu", {} }, + .{ "priv.hu", {} }, + .{ "reklam.hu", {} }, + .{ "sex.hu", {} }, + .{ "shop.hu", {} }, + .{ "sport.hu", {} }, + .{ "suli.hu", {} }, + .{ "szex.hu", {} }, + .{ "tm.hu", {} }, + .{ "tozsde.hu", {} }, + .{ "utazas.hu", {} }, + .{ "video.hu", {} }, + .{ "id", {} }, + .{ "ac.id", {} }, + .{ "biz.id", {} }, + .{ "co.id", {} }, + .{ "desa.id", {} }, + .{ "go.id", {} }, + .{ "mil.id", {} }, + .{ "my.id", {} }, + .{ "net.id", {} }, + .{ "or.id", {} }, + .{ "ponpes.id", {} }, + .{ "sch.id", {} }, + .{ "web.id", {} }, + .{ "ie", {} }, + .{ "gov.ie", {} }, + .{ "il", {} }, + .{ "ac.il", {} }, + .{ "co.il", {} }, + .{ "gov.il", {} }, + .{ "idf.il", {} }, + .{ "k12.il", {} }, + .{ "muni.il", {} }, + .{ "net.il", {} }, + .{ "org.il", {} }, + .{ "ישראל", {} }, + .{ "אקדמיה.ישראל", {} }, + .{ "ישוב.ישראל", {} }, + .{ "צהל.ישראל", {} }, + .{ "ממשל.ישראל", {} }, + .{ "im", {} }, + .{ "ac.im", {} }, + .{ "co.im", {} }, + .{ "ltd.co.im", {} }, + .{ "plc.co.im", {} }, + .{ "com.im", {} }, + .{ "net.im", {} }, + .{ "org.im", {} }, + .{ "tt.im", {} }, + .{ "tv.im", {} }, + .{ "in", {} }, + .{ "5g.in", {} }, + .{ "6g.in", {} }, + .{ "ac.in", {} }, + .{ "ai.in", {} }, + .{ "am.in", {} }, + .{ "bihar.in", {} }, + .{ "biz.in", {} }, + .{ "business.in", {} }, + .{ "ca.in", {} }, + .{ "cn.in", {} }, + .{ "co.in", {} }, + .{ "com.in", {} }, + .{ "coop.in", {} }, + .{ "cs.in", {} }, + .{ "delhi.in", {} }, + .{ "dr.in", {} }, + .{ "edu.in", {} }, + .{ "er.in", {} }, + .{ "firm.in", {} }, + .{ "gen.in", {} }, + .{ "gov.in", {} }, + .{ "gujarat.in", {} }, + .{ "ind.in", {} }, + .{ "info.in", {} }, + .{ "int.in", {} }, + .{ "internet.in", {} }, + .{ "io.in", {} }, + .{ "me.in", {} }, + .{ "mil.in", {} }, + .{ "net.in", {} }, + .{ "nic.in", {} }, + .{ "org.in", {} }, + .{ "pg.in", {} }, + .{ "post.in", {} }, + .{ "pro.in", {} }, + .{ "res.in", {} }, + .{ "travel.in", {} }, + .{ "tv.in", {} }, + .{ "uk.in", {} }, + .{ "up.in", {} }, + .{ "us.in", {} }, + .{ "info", {} }, + .{ "int", {} }, + .{ "eu.int", {} }, + .{ "io", {} }, + .{ "co.io", {} }, + .{ "com.io", {} }, + .{ "edu.io", {} }, + .{ "gov.io", {} }, + .{ "mil.io", {} }, + .{ "net.io", {} }, + .{ "nom.io", {} }, + .{ "org.io", {} }, + .{ "iq", {} }, + .{ "com.iq", {} }, + .{ "edu.iq", {} }, + .{ "gov.iq", {} }, + .{ "mil.iq", {} }, + .{ "net.iq", {} }, + .{ "org.iq", {} }, + .{ "ir", {} }, + .{ "ac.ir", {} }, + .{ "co.ir", {} }, + .{ "gov.ir", {} }, + .{ "id.ir", {} }, + .{ "net.ir", {} }, + .{ "org.ir", {} }, + .{ "sch.ir", {} }, + .{ "ایران.ir", {} }, + .{ "ايران.ir", {} }, + .{ "is", {} }, + .{ "it", {} }, + .{ "edu.it", {} }, + .{ "gov.it", {} }, + .{ "abr.it", {} }, + .{ "abruzzo.it", {} }, + .{ "aosta-valley.it", {} }, + .{ "aostavalley.it", {} }, + .{ "bas.it", {} }, + .{ "basilicata.it", {} }, + .{ "cal.it", {} }, + .{ "calabria.it", {} }, + .{ "cam.it", {} }, + .{ "campania.it", {} }, + .{ "emilia-romagna.it", {} }, + .{ "emiliaromagna.it", {} }, + .{ "emr.it", {} }, + .{ "friuli-v-giulia.it", {} }, + .{ "friuli-ve-giulia.it", {} }, + .{ "friuli-vegiulia.it", {} }, + .{ "friuli-venezia-giulia.it", {} }, + .{ "friuli-veneziagiulia.it", {} }, + .{ "friuli-vgiulia.it", {} }, + .{ "friuliv-giulia.it", {} }, + .{ "friulive-giulia.it", {} }, + .{ "friulivegiulia.it", {} }, + .{ "friulivenezia-giulia.it", {} }, + .{ "friuliveneziagiulia.it", {} }, + .{ "friulivgiulia.it", {} }, + .{ "fvg.it", {} }, + .{ "laz.it", {} }, + .{ "lazio.it", {} }, + .{ "lig.it", {} }, + .{ "liguria.it", {} }, + .{ "lom.it", {} }, + .{ "lombardia.it", {} }, + .{ "lombardy.it", {} }, + .{ "lucania.it", {} }, + .{ "mar.it", {} }, + .{ "marche.it", {} }, + .{ "mol.it", {} }, + .{ "molise.it", {} }, + .{ "piedmont.it", {} }, + .{ "piemonte.it", {} }, + .{ "pmn.it", {} }, + .{ "pug.it", {} }, + .{ "puglia.it", {} }, + .{ "sar.it", {} }, + .{ "sardegna.it", {} }, + .{ "sardinia.it", {} }, + .{ "sic.it", {} }, + .{ "sicilia.it", {} }, + .{ "sicily.it", {} }, + .{ "taa.it", {} }, + .{ "tos.it", {} }, + .{ "toscana.it", {} }, + .{ "trentin-sud-tirol.it", {} }, + .{ "trentin-süd-tirol.it", {} }, + .{ "trentin-sudtirol.it", {} }, + .{ "trentin-südtirol.it", {} }, + .{ "trentin-sued-tirol.it", {} }, + .{ "trentin-suedtirol.it", {} }, + .{ "trentino.it", {} }, + .{ "trentino-a-adige.it", {} }, + .{ "trentino-aadige.it", {} }, + .{ "trentino-alto-adige.it", {} }, + .{ "trentino-altoadige.it", {} }, + .{ "trentino-s-tirol.it", {} }, + .{ "trentino-stirol.it", {} }, + .{ "trentino-sud-tirol.it", {} }, + .{ "trentino-süd-tirol.it", {} }, + .{ "trentino-sudtirol.it", {} }, + .{ "trentino-südtirol.it", {} }, + .{ "trentino-sued-tirol.it", {} }, + .{ "trentino-suedtirol.it", {} }, + .{ "trentinoa-adige.it", {} }, + .{ "trentinoaadige.it", {} }, + .{ "trentinoalto-adige.it", {} }, + .{ "trentinoaltoadige.it", {} }, + .{ "trentinos-tirol.it", {} }, + .{ "trentinostirol.it", {} }, + .{ "trentinosud-tirol.it", {} }, + .{ "trentinosüd-tirol.it", {} }, + .{ "trentinosudtirol.it", {} }, + .{ "trentinosüdtirol.it", {} }, + .{ "trentinosued-tirol.it", {} }, + .{ "trentinosuedtirol.it", {} }, + .{ "trentinsud-tirol.it", {} }, + .{ "trentinsüd-tirol.it", {} }, + .{ "trentinsudtirol.it", {} }, + .{ "trentinsüdtirol.it", {} }, + .{ "trentinsued-tirol.it", {} }, + .{ "trentinsuedtirol.it", {} }, + .{ "tuscany.it", {} }, + .{ "umb.it", {} }, + .{ "umbria.it", {} }, + .{ "val-d-aosta.it", {} }, + .{ "val-daosta.it", {} }, + .{ "vald-aosta.it", {} }, + .{ "valdaosta.it", {} }, + .{ "valle-aosta.it", {} }, + .{ "valle-d-aosta.it", {} }, + .{ "valle-daosta.it", {} }, + .{ "valleaosta.it", {} }, + .{ "valled-aosta.it", {} }, + .{ "valledaosta.it", {} }, + .{ "vallee-aoste.it", {} }, + .{ "vallée-aoste.it", {} }, + .{ "vallee-d-aoste.it", {} }, + .{ "vallée-d-aoste.it", {} }, + .{ "valleeaoste.it", {} }, + .{ "valléeaoste.it", {} }, + .{ "valleedaoste.it", {} }, + .{ "valléedaoste.it", {} }, + .{ "vao.it", {} }, + .{ "vda.it", {} }, + .{ "ven.it", {} }, + .{ "veneto.it", {} }, + .{ "ag.it", {} }, + .{ "agrigento.it", {} }, + .{ "al.it", {} }, + .{ "alessandria.it", {} }, + .{ "alto-adige.it", {} }, + .{ "altoadige.it", {} }, + .{ "an.it", {} }, + .{ "ancona.it", {} }, + .{ "andria-barletta-trani.it", {} }, + .{ "andria-trani-barletta.it", {} }, + .{ "andriabarlettatrani.it", {} }, + .{ "andriatranibarletta.it", {} }, + .{ "ao.it", {} }, + .{ "aosta.it", {} }, + .{ "aoste.it", {} }, + .{ "ap.it", {} }, + .{ "aq.it", {} }, + .{ "aquila.it", {} }, + .{ "ar.it", {} }, + .{ "arezzo.it", {} }, + .{ "ascoli-piceno.it", {} }, + .{ "ascolipiceno.it", {} }, + .{ "asti.it", {} }, + .{ "at.it", {} }, + .{ "av.it", {} }, + .{ "avellino.it", {} }, + .{ "ba.it", {} }, + .{ "balsan.it", {} }, + .{ "balsan-sudtirol.it", {} }, + .{ "balsan-südtirol.it", {} }, + .{ "balsan-suedtirol.it", {} }, + .{ "bari.it", {} }, + .{ "barletta-trani-andria.it", {} }, + .{ "barlettatraniandria.it", {} }, + .{ "belluno.it", {} }, + .{ "benevento.it", {} }, + .{ "bergamo.it", {} }, + .{ "bg.it", {} }, + .{ "bi.it", {} }, + .{ "biella.it", {} }, + .{ "bl.it", {} }, + .{ "bn.it", {} }, + .{ "bo.it", {} }, + .{ "bologna.it", {} }, + .{ "bolzano.it", {} }, + .{ "bolzano-altoadige.it", {} }, + .{ "bozen.it", {} }, + .{ "bozen-sudtirol.it", {} }, + .{ "bozen-südtirol.it", {} }, + .{ "bozen-suedtirol.it", {} }, + .{ "br.it", {} }, + .{ "brescia.it", {} }, + .{ "brindisi.it", {} }, + .{ "bs.it", {} }, + .{ "bt.it", {} }, + .{ "bulsan.it", {} }, + .{ "bulsan-sudtirol.it", {} }, + .{ "bulsan-südtirol.it", {} }, + .{ "bulsan-suedtirol.it", {} }, + .{ "bz.it", {} }, + .{ "ca.it", {} }, + .{ "cagliari.it", {} }, + .{ "caltanissetta.it", {} }, + .{ "campidano-medio.it", {} }, + .{ "campidanomedio.it", {} }, + .{ "campobasso.it", {} }, + .{ "carbonia-iglesias.it", {} }, + .{ "carboniaiglesias.it", {} }, + .{ "carrara-massa.it", {} }, + .{ "carraramassa.it", {} }, + .{ "caserta.it", {} }, + .{ "catania.it", {} }, + .{ "catanzaro.it", {} }, + .{ "cb.it", {} }, + .{ "ce.it", {} }, + .{ "cesena-forli.it", {} }, + .{ "cesena-forlì.it", {} }, + .{ "cesenaforli.it", {} }, + .{ "cesenaforlì.it", {} }, + .{ "ch.it", {} }, + .{ "chieti.it", {} }, + .{ "ci.it", {} }, + .{ "cl.it", {} }, + .{ "cn.it", {} }, + .{ "co.it", {} }, + .{ "como.it", {} }, + .{ "cosenza.it", {} }, + .{ "cr.it", {} }, + .{ "cremona.it", {} }, + .{ "crotone.it", {} }, + .{ "cs.it", {} }, + .{ "ct.it", {} }, + .{ "cuneo.it", {} }, + .{ "cz.it", {} }, + .{ "dell-ogliastra.it", {} }, + .{ "dellogliastra.it", {} }, + .{ "en.it", {} }, + .{ "enna.it", {} }, + .{ "fc.it", {} }, + .{ "fe.it", {} }, + .{ "fermo.it", {} }, + .{ "ferrara.it", {} }, + .{ "fg.it", {} }, + .{ "fi.it", {} }, + .{ "firenze.it", {} }, + .{ "florence.it", {} }, + .{ "fm.it", {} }, + .{ "foggia.it", {} }, + .{ "forli-cesena.it", {} }, + .{ "forlì-cesena.it", {} }, + .{ "forlicesena.it", {} }, + .{ "forlìcesena.it", {} }, + .{ "fr.it", {} }, + .{ "frosinone.it", {} }, + .{ "ge.it", {} }, + .{ "genoa.it", {} }, + .{ "genova.it", {} }, + .{ "go.it", {} }, + .{ "gorizia.it", {} }, + .{ "gr.it", {} }, + .{ "grosseto.it", {} }, + .{ "iglesias-carbonia.it", {} }, + .{ "iglesiascarbonia.it", {} }, + .{ "im.it", {} }, + .{ "imperia.it", {} }, + .{ "is.it", {} }, + .{ "isernia.it", {} }, + .{ "kr.it", {} }, + .{ "la-spezia.it", {} }, + .{ "laquila.it", {} }, + .{ "laspezia.it", {} }, + .{ "latina.it", {} }, + .{ "lc.it", {} }, + .{ "le.it", {} }, + .{ "lecce.it", {} }, + .{ "lecco.it", {} }, + .{ "li.it", {} }, + .{ "livorno.it", {} }, + .{ "lo.it", {} }, + .{ "lodi.it", {} }, + .{ "lt.it", {} }, + .{ "lu.it", {} }, + .{ "lucca.it", {} }, + .{ "macerata.it", {} }, + .{ "mantova.it", {} }, + .{ "massa-carrara.it", {} }, + .{ "massacarrara.it", {} }, + .{ "matera.it", {} }, + .{ "mb.it", {} }, + .{ "mc.it", {} }, + .{ "me.it", {} }, + .{ "medio-campidano.it", {} }, + .{ "mediocampidano.it", {} }, + .{ "messina.it", {} }, + .{ "mi.it", {} }, + .{ "milan.it", {} }, + .{ "milano.it", {} }, + .{ "mn.it", {} }, + .{ "mo.it", {} }, + .{ "modena.it", {} }, + .{ "monza.it", {} }, + .{ "monza-brianza.it", {} }, + .{ "monza-e-della-brianza.it", {} }, + .{ "monzabrianza.it", {} }, + .{ "monzaebrianza.it", {} }, + .{ "monzaedellabrianza.it", {} }, + .{ "ms.it", {} }, + .{ "mt.it", {} }, + .{ "na.it", {} }, + .{ "naples.it", {} }, + .{ "napoli.it", {} }, + .{ "no.it", {} }, + .{ "novara.it", {} }, + .{ "nu.it", {} }, + .{ "nuoro.it", {} }, + .{ "og.it", {} }, + .{ "ogliastra.it", {} }, + .{ "olbia-tempio.it", {} }, + .{ "olbiatempio.it", {} }, + .{ "or.it", {} }, + .{ "oristano.it", {} }, + .{ "ot.it", {} }, + .{ "pa.it", {} }, + .{ "padova.it", {} }, + .{ "padua.it", {} }, + .{ "palermo.it", {} }, + .{ "parma.it", {} }, + .{ "pavia.it", {} }, + .{ "pc.it", {} }, + .{ "pd.it", {} }, + .{ "pe.it", {} }, + .{ "perugia.it", {} }, + .{ "pesaro-urbino.it", {} }, + .{ "pesarourbino.it", {} }, + .{ "pescara.it", {} }, + .{ "pg.it", {} }, + .{ "pi.it", {} }, + .{ "piacenza.it", {} }, + .{ "pisa.it", {} }, + .{ "pistoia.it", {} }, + .{ "pn.it", {} }, + .{ "po.it", {} }, + .{ "pordenone.it", {} }, + .{ "potenza.it", {} }, + .{ "pr.it", {} }, + .{ "prato.it", {} }, + .{ "pt.it", {} }, + .{ "pu.it", {} }, + .{ "pv.it", {} }, + .{ "pz.it", {} }, + .{ "ra.it", {} }, + .{ "ragusa.it", {} }, + .{ "ravenna.it", {} }, + .{ "rc.it", {} }, + .{ "re.it", {} }, + .{ "reggio-calabria.it", {} }, + .{ "reggio-emilia.it", {} }, + .{ "reggiocalabria.it", {} }, + .{ "reggioemilia.it", {} }, + .{ "rg.it", {} }, + .{ "ri.it", {} }, + .{ "rieti.it", {} }, + .{ "rimini.it", {} }, + .{ "rm.it", {} }, + .{ "rn.it", {} }, + .{ "ro.it", {} }, + .{ "roma.it", {} }, + .{ "rome.it", {} }, + .{ "rovigo.it", {} }, + .{ "sa.it", {} }, + .{ "salerno.it", {} }, + .{ "sassari.it", {} }, + .{ "savona.it", {} }, + .{ "si.it", {} }, + .{ "siena.it", {} }, + .{ "siracusa.it", {} }, + .{ "so.it", {} }, + .{ "sondrio.it", {} }, + .{ "sp.it", {} }, + .{ "sr.it", {} }, + .{ "ss.it", {} }, + .{ "südtirol.it", {} }, + .{ "suedtirol.it", {} }, + .{ "sv.it", {} }, + .{ "ta.it", {} }, + .{ "taranto.it", {} }, + .{ "te.it", {} }, + .{ "tempio-olbia.it", {} }, + .{ "tempioolbia.it", {} }, + .{ "teramo.it", {} }, + .{ "terni.it", {} }, + .{ "tn.it", {} }, + .{ "to.it", {} }, + .{ "torino.it", {} }, + .{ "tp.it", {} }, + .{ "tr.it", {} }, + .{ "trani-andria-barletta.it", {} }, + .{ "trani-barletta-andria.it", {} }, + .{ "traniandriabarletta.it", {} }, + .{ "tranibarlettaandria.it", {} }, + .{ "trapani.it", {} }, + .{ "trento.it", {} }, + .{ "treviso.it", {} }, + .{ "trieste.it", {} }, + .{ "ts.it", {} }, + .{ "turin.it", {} }, + .{ "tv.it", {} }, + .{ "ud.it", {} }, + .{ "udine.it", {} }, + .{ "urbino-pesaro.it", {} }, + .{ "urbinopesaro.it", {} }, + .{ "va.it", {} }, + .{ "varese.it", {} }, + .{ "vb.it", {} }, + .{ "vc.it", {} }, + .{ "ve.it", {} }, + .{ "venezia.it", {} }, + .{ "venice.it", {} }, + .{ "verbania.it", {} }, + .{ "vercelli.it", {} }, + .{ "verona.it", {} }, + .{ "vi.it", {} }, + .{ "vibo-valentia.it", {} }, + .{ "vibovalentia.it", {} }, + .{ "vicenza.it", {} }, + .{ "viterbo.it", {} }, + .{ "vr.it", {} }, + .{ "vs.it", {} }, + .{ "vt.it", {} }, + .{ "vv.it", {} }, + .{ "je", {} }, + .{ "co.je", {} }, + .{ "net.je", {} }, + .{ "org.je", {} }, + .{ "*.jm", {} }, + .{ "jo", {} }, + .{ "agri.jo", {} }, + .{ "ai.jo", {} }, + .{ "com.jo", {} }, + .{ "edu.jo", {} }, + .{ "eng.jo", {} }, + .{ "fm.jo", {} }, + .{ "gov.jo", {} }, + .{ "mil.jo", {} }, + .{ "net.jo", {} }, + .{ "org.jo", {} }, + .{ "per.jo", {} }, + .{ "phd.jo", {} }, + .{ "sch.jo", {} }, + .{ "tv.jo", {} }, + .{ "jobs", {} }, + .{ "jp", {} }, + .{ "ac.jp", {} }, + .{ "ad.jp", {} }, + .{ "co.jp", {} }, + .{ "ed.jp", {} }, + .{ "go.jp", {} }, + .{ "gr.jp", {} }, + .{ "lg.jp", {} }, + .{ "ne.jp", {} }, + .{ "or.jp", {} }, + .{ "aichi.jp", {} }, + .{ "akita.jp", {} }, + .{ "aomori.jp", {} }, + .{ "chiba.jp", {} }, + .{ "ehime.jp", {} }, + .{ "fukui.jp", {} }, + .{ "fukuoka.jp", {} }, + .{ "fukushima.jp", {} }, + .{ "gifu.jp", {} }, + .{ "gunma.jp", {} }, + .{ "hiroshima.jp", {} }, + .{ "hokkaido.jp", {} }, + .{ "hyogo.jp", {} }, + .{ "ibaraki.jp", {} }, + .{ "ishikawa.jp", {} }, + .{ "iwate.jp", {} }, + .{ "kagawa.jp", {} }, + .{ "kagoshima.jp", {} }, + .{ "kanagawa.jp", {} }, + .{ "kochi.jp", {} }, + .{ "kumamoto.jp", {} }, + .{ "kyoto.jp", {} }, + .{ "mie.jp", {} }, + .{ "miyagi.jp", {} }, + .{ "miyazaki.jp", {} }, + .{ "nagano.jp", {} }, + .{ "nagasaki.jp", {} }, + .{ "nara.jp", {} }, + .{ "niigata.jp", {} }, + .{ "oita.jp", {} }, + .{ "okayama.jp", {} }, + .{ "okinawa.jp", {} }, + .{ "osaka.jp", {} }, + .{ "saga.jp", {} }, + .{ "saitama.jp", {} }, + .{ "shiga.jp", {} }, + .{ "shimane.jp", {} }, + .{ "shizuoka.jp", {} }, + .{ "tochigi.jp", {} }, + .{ "tokushima.jp", {} }, + .{ "tokyo.jp", {} }, + .{ "tottori.jp", {} }, + .{ "toyama.jp", {} }, + .{ "wakayama.jp", {} }, + .{ "yamagata.jp", {} }, + .{ "yamaguchi.jp", {} }, + .{ "yamanashi.jp", {} }, + .{ "三重.jp", {} }, + .{ "京都.jp", {} }, + .{ "佐賀.jp", {} }, + .{ "兵庫.jp", {} }, + .{ "北海道.jp", {} }, + .{ "千葉.jp", {} }, + .{ "和歌山.jp", {} }, + .{ "埼玉.jp", {} }, + .{ "大分.jp", {} }, + .{ "大阪.jp", {} }, + .{ "奈良.jp", {} }, + .{ "宮城.jp", {} }, + .{ "宮崎.jp", {} }, + .{ "富山.jp", {} }, + .{ "山口.jp", {} }, + .{ "山形.jp", {} }, + .{ "山梨.jp", {} }, + .{ "岐阜.jp", {} }, + .{ "岡山.jp", {} }, + .{ "岩手.jp", {} }, + .{ "島根.jp", {} }, + .{ "広島.jp", {} }, + .{ "徳島.jp", {} }, + .{ "愛媛.jp", {} }, + .{ "愛知.jp", {} }, + .{ "新潟.jp", {} }, + .{ "東京.jp", {} }, + .{ "栃木.jp", {} }, + .{ "沖縄.jp", {} }, + .{ "滋賀.jp", {} }, + .{ "熊本.jp", {} }, + .{ "石川.jp", {} }, + .{ "神奈川.jp", {} }, + .{ "福井.jp", {} }, + .{ "福岡.jp", {} }, + .{ "福島.jp", {} }, + .{ "秋田.jp", {} }, + .{ "群馬.jp", {} }, + .{ "茨城.jp", {} }, + .{ "長崎.jp", {} }, + .{ "長野.jp", {} }, + .{ "青森.jp", {} }, + .{ "静岡.jp", {} }, + .{ "香川.jp", {} }, + .{ "高知.jp", {} }, + .{ "鳥取.jp", {} }, + .{ "鹿児島.jp", {} }, + .{ "*.kawasaki.jp", {} }, + .{ "!city.kawasaki.jp", {} }, + .{ "*.kitakyushu.jp", {} }, + .{ "!city.kitakyushu.jp", {} }, + .{ "*.kobe.jp", {} }, + .{ "!city.kobe.jp", {} }, + .{ "*.nagoya.jp", {} }, + .{ "!city.nagoya.jp", {} }, + .{ "*.sapporo.jp", {} }, + .{ "!city.sapporo.jp", {} }, + .{ "*.sendai.jp", {} }, + .{ "!city.sendai.jp", {} }, + .{ "*.yokohama.jp", {} }, + .{ "!city.yokohama.jp", {} }, + .{ "aisai.aichi.jp", {} }, + .{ "ama.aichi.jp", {} }, + .{ "anjo.aichi.jp", {} }, + .{ "asuke.aichi.jp", {} }, + .{ "chiryu.aichi.jp", {} }, + .{ "chita.aichi.jp", {} }, + .{ "fuso.aichi.jp", {} }, + .{ "gamagori.aichi.jp", {} }, + .{ "handa.aichi.jp", {} }, + .{ "hazu.aichi.jp", {} }, + .{ "hekinan.aichi.jp", {} }, + .{ "higashiura.aichi.jp", {} }, + .{ "ichinomiya.aichi.jp", {} }, + .{ "inazawa.aichi.jp", {} }, + .{ "inuyama.aichi.jp", {} }, + .{ "isshiki.aichi.jp", {} }, + .{ "iwakura.aichi.jp", {} }, + .{ "kanie.aichi.jp", {} }, + .{ "kariya.aichi.jp", {} }, + .{ "kasugai.aichi.jp", {} }, + .{ "kira.aichi.jp", {} }, + .{ "kiyosu.aichi.jp", {} }, + .{ "komaki.aichi.jp", {} }, + .{ "konan.aichi.jp", {} }, + .{ "kota.aichi.jp", {} }, + .{ "mihama.aichi.jp", {} }, + .{ "miyoshi.aichi.jp", {} }, + .{ "nishio.aichi.jp", {} }, + .{ "nisshin.aichi.jp", {} }, + .{ "obu.aichi.jp", {} }, + .{ "oguchi.aichi.jp", {} }, + .{ "oharu.aichi.jp", {} }, + .{ "okazaki.aichi.jp", {} }, + .{ "owariasahi.aichi.jp", {} }, + .{ "seto.aichi.jp", {} }, + .{ "shikatsu.aichi.jp", {} }, + .{ "shinshiro.aichi.jp", {} }, + .{ "shitara.aichi.jp", {} }, + .{ "tahara.aichi.jp", {} }, + .{ "takahama.aichi.jp", {} }, + .{ "tobishima.aichi.jp", {} }, + .{ "toei.aichi.jp", {} }, + .{ "togo.aichi.jp", {} }, + .{ "tokai.aichi.jp", {} }, + .{ "tokoname.aichi.jp", {} }, + .{ "toyoake.aichi.jp", {} }, + .{ "toyohashi.aichi.jp", {} }, + .{ "toyokawa.aichi.jp", {} }, + .{ "toyone.aichi.jp", {} }, + .{ "toyota.aichi.jp", {} }, + .{ "tsushima.aichi.jp", {} }, + .{ "yatomi.aichi.jp", {} }, + .{ "akita.akita.jp", {} }, + .{ "daisen.akita.jp", {} }, + .{ "fujisato.akita.jp", {} }, + .{ "gojome.akita.jp", {} }, + .{ "hachirogata.akita.jp", {} }, + .{ "happou.akita.jp", {} }, + .{ "higashinaruse.akita.jp", {} }, + .{ "honjo.akita.jp", {} }, + .{ "honjyo.akita.jp", {} }, + .{ "ikawa.akita.jp", {} }, + .{ "kamikoani.akita.jp", {} }, + .{ "kamioka.akita.jp", {} }, + .{ "katagami.akita.jp", {} }, + .{ "kazuno.akita.jp", {} }, + .{ "kitaakita.akita.jp", {} }, + .{ "kosaka.akita.jp", {} }, + .{ "kyowa.akita.jp", {} }, + .{ "misato.akita.jp", {} }, + .{ "mitane.akita.jp", {} }, + .{ "moriyoshi.akita.jp", {} }, + .{ "nikaho.akita.jp", {} }, + .{ "noshiro.akita.jp", {} }, + .{ "odate.akita.jp", {} }, + .{ "oga.akita.jp", {} }, + .{ "ogata.akita.jp", {} }, + .{ "semboku.akita.jp", {} }, + .{ "yokote.akita.jp", {} }, + .{ "yurihonjo.akita.jp", {} }, + .{ "aomori.aomori.jp", {} }, + .{ "gonohe.aomori.jp", {} }, + .{ "hachinohe.aomori.jp", {} }, + .{ "hashikami.aomori.jp", {} }, + .{ "hiranai.aomori.jp", {} }, + .{ "hirosaki.aomori.jp", {} }, + .{ "itayanagi.aomori.jp", {} }, + .{ "kuroishi.aomori.jp", {} }, + .{ "misawa.aomori.jp", {} }, + .{ "mutsu.aomori.jp", {} }, + .{ "nakadomari.aomori.jp", {} }, + .{ "noheji.aomori.jp", {} }, + .{ "oirase.aomori.jp", {} }, + .{ "owani.aomori.jp", {} }, + .{ "rokunohe.aomori.jp", {} }, + .{ "sannohe.aomori.jp", {} }, + .{ "shichinohe.aomori.jp", {} }, + .{ "shingo.aomori.jp", {} }, + .{ "takko.aomori.jp", {} }, + .{ "towada.aomori.jp", {} }, + .{ "tsugaru.aomori.jp", {} }, + .{ "tsuruta.aomori.jp", {} }, + .{ "abiko.chiba.jp", {} }, + .{ "asahi.chiba.jp", {} }, + .{ "chonan.chiba.jp", {} }, + .{ "chosei.chiba.jp", {} }, + .{ "choshi.chiba.jp", {} }, + .{ "chuo.chiba.jp", {} }, + .{ "funabashi.chiba.jp", {} }, + .{ "futtsu.chiba.jp", {} }, + .{ "hanamigawa.chiba.jp", {} }, + .{ "ichihara.chiba.jp", {} }, + .{ "ichikawa.chiba.jp", {} }, + .{ "ichinomiya.chiba.jp", {} }, + .{ "inzai.chiba.jp", {} }, + .{ "isumi.chiba.jp", {} }, + .{ "kamagaya.chiba.jp", {} }, + .{ "kamogawa.chiba.jp", {} }, + .{ "kashiwa.chiba.jp", {} }, + .{ "katori.chiba.jp", {} }, + .{ "katsuura.chiba.jp", {} }, + .{ "kimitsu.chiba.jp", {} }, + .{ "kisarazu.chiba.jp", {} }, + .{ "kozaki.chiba.jp", {} }, + .{ "kujukuri.chiba.jp", {} }, + .{ "kyonan.chiba.jp", {} }, + .{ "matsudo.chiba.jp", {} }, + .{ "midori.chiba.jp", {} }, + .{ "mihama.chiba.jp", {} }, + .{ "minamiboso.chiba.jp", {} }, + .{ "mobara.chiba.jp", {} }, + .{ "mutsuzawa.chiba.jp", {} }, + .{ "nagara.chiba.jp", {} }, + .{ "nagareyama.chiba.jp", {} }, + .{ "narashino.chiba.jp", {} }, + .{ "narita.chiba.jp", {} }, + .{ "noda.chiba.jp", {} }, + .{ "oamishirasato.chiba.jp", {} }, + .{ "omigawa.chiba.jp", {} }, + .{ "onjuku.chiba.jp", {} }, + .{ "otaki.chiba.jp", {} }, + .{ "sakae.chiba.jp", {} }, + .{ "sakura.chiba.jp", {} }, + .{ "shimofusa.chiba.jp", {} }, + .{ "shirako.chiba.jp", {} }, + .{ "shiroi.chiba.jp", {} }, + .{ "shisui.chiba.jp", {} }, + .{ "sodegaura.chiba.jp", {} }, + .{ "sosa.chiba.jp", {} }, + .{ "tako.chiba.jp", {} }, + .{ "tateyama.chiba.jp", {} }, + .{ "togane.chiba.jp", {} }, + .{ "tohnosho.chiba.jp", {} }, + .{ "tomisato.chiba.jp", {} }, + .{ "urayasu.chiba.jp", {} }, + .{ "yachimata.chiba.jp", {} }, + .{ "yachiyo.chiba.jp", {} }, + .{ "yokaichiba.chiba.jp", {} }, + .{ "yokoshibahikari.chiba.jp", {} }, + .{ "yotsukaido.chiba.jp", {} }, + .{ "ainan.ehime.jp", {} }, + .{ "honai.ehime.jp", {} }, + .{ "ikata.ehime.jp", {} }, + .{ "imabari.ehime.jp", {} }, + .{ "iyo.ehime.jp", {} }, + .{ "kamijima.ehime.jp", {} }, + .{ "kihoku.ehime.jp", {} }, + .{ "kumakogen.ehime.jp", {} }, + .{ "masaki.ehime.jp", {} }, + .{ "matsuno.ehime.jp", {} }, + .{ "matsuyama.ehime.jp", {} }, + .{ "namikata.ehime.jp", {} }, + .{ "niihama.ehime.jp", {} }, + .{ "ozu.ehime.jp", {} }, + .{ "saijo.ehime.jp", {} }, + .{ "seiyo.ehime.jp", {} }, + .{ "shikokuchuo.ehime.jp", {} }, + .{ "tobe.ehime.jp", {} }, + .{ "toon.ehime.jp", {} }, + .{ "uchiko.ehime.jp", {} }, + .{ "uwajima.ehime.jp", {} }, + .{ "yawatahama.ehime.jp", {} }, + .{ "echizen.fukui.jp", {} }, + .{ "eiheiji.fukui.jp", {} }, + .{ "fukui.fukui.jp", {} }, + .{ "ikeda.fukui.jp", {} }, + .{ "katsuyama.fukui.jp", {} }, + .{ "mihama.fukui.jp", {} }, + .{ "minamiechizen.fukui.jp", {} }, + .{ "obama.fukui.jp", {} }, + .{ "ohi.fukui.jp", {} }, + .{ "ono.fukui.jp", {} }, + .{ "sabae.fukui.jp", {} }, + .{ "sakai.fukui.jp", {} }, + .{ "takahama.fukui.jp", {} }, + .{ "tsuruga.fukui.jp", {} }, + .{ "wakasa.fukui.jp", {} }, + .{ "ashiya.fukuoka.jp", {} }, + .{ "buzen.fukuoka.jp", {} }, + .{ "chikugo.fukuoka.jp", {} }, + .{ "chikuho.fukuoka.jp", {} }, + .{ "chikujo.fukuoka.jp", {} }, + .{ "chikushino.fukuoka.jp", {} }, + .{ "chikuzen.fukuoka.jp", {} }, + .{ "chuo.fukuoka.jp", {} }, + .{ "dazaifu.fukuoka.jp", {} }, + .{ "fukuchi.fukuoka.jp", {} }, + .{ "hakata.fukuoka.jp", {} }, + .{ "higashi.fukuoka.jp", {} }, + .{ "hirokawa.fukuoka.jp", {} }, + .{ "hisayama.fukuoka.jp", {} }, + .{ "iizuka.fukuoka.jp", {} }, + .{ "inatsuki.fukuoka.jp", {} }, + .{ "kaho.fukuoka.jp", {} }, + .{ "kasuga.fukuoka.jp", {} }, + .{ "kasuya.fukuoka.jp", {} }, + .{ "kawara.fukuoka.jp", {} }, + .{ "keisen.fukuoka.jp", {} }, + .{ "koga.fukuoka.jp", {} }, + .{ "kurate.fukuoka.jp", {} }, + .{ "kurogi.fukuoka.jp", {} }, + .{ "kurume.fukuoka.jp", {} }, + .{ "minami.fukuoka.jp", {} }, + .{ "miyako.fukuoka.jp", {} }, + .{ "miyama.fukuoka.jp", {} }, + .{ "miyawaka.fukuoka.jp", {} }, + .{ "mizumaki.fukuoka.jp", {} }, + .{ "munakata.fukuoka.jp", {} }, + .{ "nakagawa.fukuoka.jp", {} }, + .{ "nakama.fukuoka.jp", {} }, + .{ "nishi.fukuoka.jp", {} }, + .{ "nogata.fukuoka.jp", {} }, + .{ "ogori.fukuoka.jp", {} }, + .{ "okagaki.fukuoka.jp", {} }, + .{ "okawa.fukuoka.jp", {} }, + .{ "oki.fukuoka.jp", {} }, + .{ "omuta.fukuoka.jp", {} }, + .{ "onga.fukuoka.jp", {} }, + .{ "onojo.fukuoka.jp", {} }, + .{ "oto.fukuoka.jp", {} }, + .{ "saigawa.fukuoka.jp", {} }, + .{ "sasaguri.fukuoka.jp", {} }, + .{ "shingu.fukuoka.jp", {} }, + .{ "shinyoshitomi.fukuoka.jp", {} }, + .{ "shonai.fukuoka.jp", {} }, + .{ "soeda.fukuoka.jp", {} }, + .{ "sue.fukuoka.jp", {} }, + .{ "tachiarai.fukuoka.jp", {} }, + .{ "tagawa.fukuoka.jp", {} }, + .{ "takata.fukuoka.jp", {} }, + .{ "toho.fukuoka.jp", {} }, + .{ "toyotsu.fukuoka.jp", {} }, + .{ "tsuiki.fukuoka.jp", {} }, + .{ "ukiha.fukuoka.jp", {} }, + .{ "umi.fukuoka.jp", {} }, + .{ "usui.fukuoka.jp", {} }, + .{ "yamada.fukuoka.jp", {} }, + .{ "yame.fukuoka.jp", {} }, + .{ "yanagawa.fukuoka.jp", {} }, + .{ "yukuhashi.fukuoka.jp", {} }, + .{ "aizubange.fukushima.jp", {} }, + .{ "aizumisato.fukushima.jp", {} }, + .{ "aizuwakamatsu.fukushima.jp", {} }, + .{ "asakawa.fukushima.jp", {} }, + .{ "bandai.fukushima.jp", {} }, + .{ "date.fukushima.jp", {} }, + .{ "fukushima.fukushima.jp", {} }, + .{ "furudono.fukushima.jp", {} }, + .{ "futaba.fukushima.jp", {} }, + .{ "hanawa.fukushima.jp", {} }, + .{ "higashi.fukushima.jp", {} }, + .{ "hirata.fukushima.jp", {} }, + .{ "hirono.fukushima.jp", {} }, + .{ "iitate.fukushima.jp", {} }, + .{ "inawashiro.fukushima.jp", {} }, + .{ "ishikawa.fukushima.jp", {} }, + .{ "iwaki.fukushima.jp", {} }, + .{ "izumizaki.fukushima.jp", {} }, + .{ "kagamiishi.fukushima.jp", {} }, + .{ "kaneyama.fukushima.jp", {} }, + .{ "kawamata.fukushima.jp", {} }, + .{ "kitakata.fukushima.jp", {} }, + .{ "kitashiobara.fukushima.jp", {} }, + .{ "koori.fukushima.jp", {} }, + .{ "koriyama.fukushima.jp", {} }, + .{ "kunimi.fukushima.jp", {} }, + .{ "miharu.fukushima.jp", {} }, + .{ "mishima.fukushima.jp", {} }, + .{ "namie.fukushima.jp", {} }, + .{ "nango.fukushima.jp", {} }, + .{ "nishiaizu.fukushima.jp", {} }, + .{ "nishigo.fukushima.jp", {} }, + .{ "okuma.fukushima.jp", {} }, + .{ "omotego.fukushima.jp", {} }, + .{ "ono.fukushima.jp", {} }, + .{ "otama.fukushima.jp", {} }, + .{ "samegawa.fukushima.jp", {} }, + .{ "shimogo.fukushima.jp", {} }, + .{ "shirakawa.fukushima.jp", {} }, + .{ "showa.fukushima.jp", {} }, + .{ "soma.fukushima.jp", {} }, + .{ "sukagawa.fukushima.jp", {} }, + .{ "taishin.fukushima.jp", {} }, + .{ "tamakawa.fukushima.jp", {} }, + .{ "tanagura.fukushima.jp", {} }, + .{ "tenei.fukushima.jp", {} }, + .{ "yabuki.fukushima.jp", {} }, + .{ "yamato.fukushima.jp", {} }, + .{ "yamatsuri.fukushima.jp", {} }, + .{ "yanaizu.fukushima.jp", {} }, + .{ "yugawa.fukushima.jp", {} }, + .{ "anpachi.gifu.jp", {} }, + .{ "ena.gifu.jp", {} }, + .{ "gifu.gifu.jp", {} }, + .{ "ginan.gifu.jp", {} }, + .{ "godo.gifu.jp", {} }, + .{ "gujo.gifu.jp", {} }, + .{ "hashima.gifu.jp", {} }, + .{ "hichiso.gifu.jp", {} }, + .{ "hida.gifu.jp", {} }, + .{ "higashishirakawa.gifu.jp", {} }, + .{ "ibigawa.gifu.jp", {} }, + .{ "ikeda.gifu.jp", {} }, + .{ "kakamigahara.gifu.jp", {} }, + .{ "kani.gifu.jp", {} }, + .{ "kasahara.gifu.jp", {} }, + .{ "kasamatsu.gifu.jp", {} }, + .{ "kawaue.gifu.jp", {} }, + .{ "kitagata.gifu.jp", {} }, + .{ "mino.gifu.jp", {} }, + .{ "minokamo.gifu.jp", {} }, + .{ "mitake.gifu.jp", {} }, + .{ "mizunami.gifu.jp", {} }, + .{ "motosu.gifu.jp", {} }, + .{ "nakatsugawa.gifu.jp", {} }, + .{ "ogaki.gifu.jp", {} }, + .{ "sakahogi.gifu.jp", {} }, + .{ "seki.gifu.jp", {} }, + .{ "sekigahara.gifu.jp", {} }, + .{ "shirakawa.gifu.jp", {} }, + .{ "tajimi.gifu.jp", {} }, + .{ "takayama.gifu.jp", {} }, + .{ "tarui.gifu.jp", {} }, + .{ "toki.gifu.jp", {} }, + .{ "tomika.gifu.jp", {} }, + .{ "wanouchi.gifu.jp", {} }, + .{ "yamagata.gifu.jp", {} }, + .{ "yaotsu.gifu.jp", {} }, + .{ "yoro.gifu.jp", {} }, + .{ "annaka.gunma.jp", {} }, + .{ "chiyoda.gunma.jp", {} }, + .{ "fujioka.gunma.jp", {} }, + .{ "higashiagatsuma.gunma.jp", {} }, + .{ "isesaki.gunma.jp", {} }, + .{ "itakura.gunma.jp", {} }, + .{ "kanna.gunma.jp", {} }, + .{ "kanra.gunma.jp", {} }, + .{ "katashina.gunma.jp", {} }, + .{ "kawaba.gunma.jp", {} }, + .{ "kiryu.gunma.jp", {} }, + .{ "kusatsu.gunma.jp", {} }, + .{ "maebashi.gunma.jp", {} }, + .{ "meiwa.gunma.jp", {} }, + .{ "midori.gunma.jp", {} }, + .{ "minakami.gunma.jp", {} }, + .{ "naganohara.gunma.jp", {} }, + .{ "nakanojo.gunma.jp", {} }, + .{ "nanmoku.gunma.jp", {} }, + .{ "numata.gunma.jp", {} }, + .{ "oizumi.gunma.jp", {} }, + .{ "ora.gunma.jp", {} }, + .{ "ota.gunma.jp", {} }, + .{ "shibukawa.gunma.jp", {} }, + .{ "shimonita.gunma.jp", {} }, + .{ "shinto.gunma.jp", {} }, + .{ "showa.gunma.jp", {} }, + .{ "takasaki.gunma.jp", {} }, + .{ "takayama.gunma.jp", {} }, + .{ "tamamura.gunma.jp", {} }, + .{ "tatebayashi.gunma.jp", {} }, + .{ "tomioka.gunma.jp", {} }, + .{ "tsukiyono.gunma.jp", {} }, + .{ "tsumagoi.gunma.jp", {} }, + .{ "ueno.gunma.jp", {} }, + .{ "yoshioka.gunma.jp", {} }, + .{ "asaminami.hiroshima.jp", {} }, + .{ "daiwa.hiroshima.jp", {} }, + .{ "etajima.hiroshima.jp", {} }, + .{ "fuchu.hiroshima.jp", {} }, + .{ "fukuyama.hiroshima.jp", {} }, + .{ "hatsukaichi.hiroshima.jp", {} }, + .{ "higashihiroshima.hiroshima.jp", {} }, + .{ "hongo.hiroshima.jp", {} }, + .{ "jinsekikogen.hiroshima.jp", {} }, + .{ "kaita.hiroshima.jp", {} }, + .{ "kui.hiroshima.jp", {} }, + .{ "kumano.hiroshima.jp", {} }, + .{ "kure.hiroshima.jp", {} }, + .{ "mihara.hiroshima.jp", {} }, + .{ "miyoshi.hiroshima.jp", {} }, + .{ "naka.hiroshima.jp", {} }, + .{ "onomichi.hiroshima.jp", {} }, + .{ "osakikamijima.hiroshima.jp", {} }, + .{ "otake.hiroshima.jp", {} }, + .{ "saka.hiroshima.jp", {} }, + .{ "sera.hiroshima.jp", {} }, + .{ "seranishi.hiroshima.jp", {} }, + .{ "shinichi.hiroshima.jp", {} }, + .{ "shobara.hiroshima.jp", {} }, + .{ "takehara.hiroshima.jp", {} }, + .{ "abashiri.hokkaido.jp", {} }, + .{ "abira.hokkaido.jp", {} }, + .{ "aibetsu.hokkaido.jp", {} }, + .{ "akabira.hokkaido.jp", {} }, + .{ "akkeshi.hokkaido.jp", {} }, + .{ "asahikawa.hokkaido.jp", {} }, + .{ "ashibetsu.hokkaido.jp", {} }, + .{ "ashoro.hokkaido.jp", {} }, + .{ "assabu.hokkaido.jp", {} }, + .{ "atsuma.hokkaido.jp", {} }, + .{ "bibai.hokkaido.jp", {} }, + .{ "biei.hokkaido.jp", {} }, + .{ "bifuka.hokkaido.jp", {} }, + .{ "bihoro.hokkaido.jp", {} }, + .{ "biratori.hokkaido.jp", {} }, + .{ "chippubetsu.hokkaido.jp", {} }, + .{ "chitose.hokkaido.jp", {} }, + .{ "date.hokkaido.jp", {} }, + .{ "ebetsu.hokkaido.jp", {} }, + .{ "embetsu.hokkaido.jp", {} }, + .{ "eniwa.hokkaido.jp", {} }, + .{ "erimo.hokkaido.jp", {} }, + .{ "esan.hokkaido.jp", {} }, + .{ "esashi.hokkaido.jp", {} }, + .{ "fukagawa.hokkaido.jp", {} }, + .{ "fukushima.hokkaido.jp", {} }, + .{ "furano.hokkaido.jp", {} }, + .{ "furubira.hokkaido.jp", {} }, + .{ "haboro.hokkaido.jp", {} }, + .{ "hakodate.hokkaido.jp", {} }, + .{ "hamatonbetsu.hokkaido.jp", {} }, + .{ "hidaka.hokkaido.jp", {} }, + .{ "higashikagura.hokkaido.jp", {} }, + .{ "higashikawa.hokkaido.jp", {} }, + .{ "hiroo.hokkaido.jp", {} }, + .{ "hokuryu.hokkaido.jp", {} }, + .{ "hokuto.hokkaido.jp", {} }, + .{ "honbetsu.hokkaido.jp", {} }, + .{ "horokanai.hokkaido.jp", {} }, + .{ "horonobe.hokkaido.jp", {} }, + .{ "ikeda.hokkaido.jp", {} }, + .{ "imakane.hokkaido.jp", {} }, + .{ "ishikari.hokkaido.jp", {} }, + .{ "iwamizawa.hokkaido.jp", {} }, + .{ "iwanai.hokkaido.jp", {} }, + .{ "kamifurano.hokkaido.jp", {} }, + .{ "kamikawa.hokkaido.jp", {} }, + .{ "kamishihoro.hokkaido.jp", {} }, + .{ "kamisunagawa.hokkaido.jp", {} }, + .{ "kamoenai.hokkaido.jp", {} }, + .{ "kayabe.hokkaido.jp", {} }, + .{ "kembuchi.hokkaido.jp", {} }, + .{ "kikonai.hokkaido.jp", {} }, + .{ "kimobetsu.hokkaido.jp", {} }, + .{ "kitahiroshima.hokkaido.jp", {} }, + .{ "kitami.hokkaido.jp", {} }, + .{ "kiyosato.hokkaido.jp", {} }, + .{ "koshimizu.hokkaido.jp", {} }, + .{ "kunneppu.hokkaido.jp", {} }, + .{ "kuriyama.hokkaido.jp", {} }, + .{ "kuromatsunai.hokkaido.jp", {} }, + .{ "kushiro.hokkaido.jp", {} }, + .{ "kutchan.hokkaido.jp", {} }, + .{ "kyowa.hokkaido.jp", {} }, + .{ "mashike.hokkaido.jp", {} }, + .{ "matsumae.hokkaido.jp", {} }, + .{ "mikasa.hokkaido.jp", {} }, + .{ "minamifurano.hokkaido.jp", {} }, + .{ "mombetsu.hokkaido.jp", {} }, + .{ "moseushi.hokkaido.jp", {} }, + .{ "mukawa.hokkaido.jp", {} }, + .{ "muroran.hokkaido.jp", {} }, + .{ "naie.hokkaido.jp", {} }, + .{ "nakagawa.hokkaido.jp", {} }, + .{ "nakasatsunai.hokkaido.jp", {} }, + .{ "nakatombetsu.hokkaido.jp", {} }, + .{ "nanae.hokkaido.jp", {} }, + .{ "nanporo.hokkaido.jp", {} }, + .{ "nayoro.hokkaido.jp", {} }, + .{ "nemuro.hokkaido.jp", {} }, + .{ "niikappu.hokkaido.jp", {} }, + .{ "niki.hokkaido.jp", {} }, + .{ "nishiokoppe.hokkaido.jp", {} }, + .{ "noboribetsu.hokkaido.jp", {} }, + .{ "numata.hokkaido.jp", {} }, + .{ "obihiro.hokkaido.jp", {} }, + .{ "obira.hokkaido.jp", {} }, + .{ "oketo.hokkaido.jp", {} }, + .{ "okoppe.hokkaido.jp", {} }, + .{ "otaru.hokkaido.jp", {} }, + .{ "otobe.hokkaido.jp", {} }, + .{ "otofuke.hokkaido.jp", {} }, + .{ "otoineppu.hokkaido.jp", {} }, + .{ "oumu.hokkaido.jp", {} }, + .{ "ozora.hokkaido.jp", {} }, + .{ "pippu.hokkaido.jp", {} }, + .{ "rankoshi.hokkaido.jp", {} }, + .{ "rebun.hokkaido.jp", {} }, + .{ "rikubetsu.hokkaido.jp", {} }, + .{ "rishiri.hokkaido.jp", {} }, + .{ "rishirifuji.hokkaido.jp", {} }, + .{ "saroma.hokkaido.jp", {} }, + .{ "sarufutsu.hokkaido.jp", {} }, + .{ "shakotan.hokkaido.jp", {} }, + .{ "shari.hokkaido.jp", {} }, + .{ "shibecha.hokkaido.jp", {} }, + .{ "shibetsu.hokkaido.jp", {} }, + .{ "shikabe.hokkaido.jp", {} }, + .{ "shikaoi.hokkaido.jp", {} }, + .{ "shimamaki.hokkaido.jp", {} }, + .{ "shimizu.hokkaido.jp", {} }, + .{ "shimokawa.hokkaido.jp", {} }, + .{ "shinshinotsu.hokkaido.jp", {} }, + .{ "shintoku.hokkaido.jp", {} }, + .{ "shiranuka.hokkaido.jp", {} }, + .{ "shiraoi.hokkaido.jp", {} }, + .{ "shiriuchi.hokkaido.jp", {} }, + .{ "sobetsu.hokkaido.jp", {} }, + .{ "sunagawa.hokkaido.jp", {} }, + .{ "taiki.hokkaido.jp", {} }, + .{ "takasu.hokkaido.jp", {} }, + .{ "takikawa.hokkaido.jp", {} }, + .{ "takinoue.hokkaido.jp", {} }, + .{ "teshikaga.hokkaido.jp", {} }, + .{ "tobetsu.hokkaido.jp", {} }, + .{ "tohma.hokkaido.jp", {} }, + .{ "tomakomai.hokkaido.jp", {} }, + .{ "tomari.hokkaido.jp", {} }, + .{ "toya.hokkaido.jp", {} }, + .{ "toyako.hokkaido.jp", {} }, + .{ "toyotomi.hokkaido.jp", {} }, + .{ "toyoura.hokkaido.jp", {} }, + .{ "tsubetsu.hokkaido.jp", {} }, + .{ "tsukigata.hokkaido.jp", {} }, + .{ "urakawa.hokkaido.jp", {} }, + .{ "urausu.hokkaido.jp", {} }, + .{ "uryu.hokkaido.jp", {} }, + .{ "utashinai.hokkaido.jp", {} }, + .{ "wakkanai.hokkaido.jp", {} }, + .{ "wassamu.hokkaido.jp", {} }, + .{ "yakumo.hokkaido.jp", {} }, + .{ "yoichi.hokkaido.jp", {} }, + .{ "aioi.hyogo.jp", {} }, + .{ "akashi.hyogo.jp", {} }, + .{ "ako.hyogo.jp", {} }, + .{ "amagasaki.hyogo.jp", {} }, + .{ "aogaki.hyogo.jp", {} }, + .{ "asago.hyogo.jp", {} }, + .{ "ashiya.hyogo.jp", {} }, + .{ "awaji.hyogo.jp", {} }, + .{ "fukusaki.hyogo.jp", {} }, + .{ "goshiki.hyogo.jp", {} }, + .{ "harima.hyogo.jp", {} }, + .{ "himeji.hyogo.jp", {} }, + .{ "ichikawa.hyogo.jp", {} }, + .{ "inagawa.hyogo.jp", {} }, + .{ "itami.hyogo.jp", {} }, + .{ "kakogawa.hyogo.jp", {} }, + .{ "kamigori.hyogo.jp", {} }, + .{ "kamikawa.hyogo.jp", {} }, + .{ "kasai.hyogo.jp", {} }, + .{ "kasuga.hyogo.jp", {} }, + .{ "kawanishi.hyogo.jp", {} }, + .{ "miki.hyogo.jp", {} }, + .{ "minamiawaji.hyogo.jp", {} }, + .{ "nishinomiya.hyogo.jp", {} }, + .{ "nishiwaki.hyogo.jp", {} }, + .{ "ono.hyogo.jp", {} }, + .{ "sanda.hyogo.jp", {} }, + .{ "sannan.hyogo.jp", {} }, + .{ "sasayama.hyogo.jp", {} }, + .{ "sayo.hyogo.jp", {} }, + .{ "shingu.hyogo.jp", {} }, + .{ "shinonsen.hyogo.jp", {} }, + .{ "shiso.hyogo.jp", {} }, + .{ "sumoto.hyogo.jp", {} }, + .{ "taishi.hyogo.jp", {} }, + .{ "taka.hyogo.jp", {} }, + .{ "takarazuka.hyogo.jp", {} }, + .{ "takasago.hyogo.jp", {} }, + .{ "takino.hyogo.jp", {} }, + .{ "tamba.hyogo.jp", {} }, + .{ "tatsuno.hyogo.jp", {} }, + .{ "toyooka.hyogo.jp", {} }, + .{ "yabu.hyogo.jp", {} }, + .{ "yashiro.hyogo.jp", {} }, + .{ "yoka.hyogo.jp", {} }, + .{ "yokawa.hyogo.jp", {} }, + .{ "ami.ibaraki.jp", {} }, + .{ "asahi.ibaraki.jp", {} }, + .{ "bando.ibaraki.jp", {} }, + .{ "chikusei.ibaraki.jp", {} }, + .{ "daigo.ibaraki.jp", {} }, + .{ "fujishiro.ibaraki.jp", {} }, + .{ "hitachi.ibaraki.jp", {} }, + .{ "hitachinaka.ibaraki.jp", {} }, + .{ "hitachiomiya.ibaraki.jp", {} }, + .{ "hitachiota.ibaraki.jp", {} }, + .{ "ibaraki.ibaraki.jp", {} }, + .{ "ina.ibaraki.jp", {} }, + .{ "inashiki.ibaraki.jp", {} }, + .{ "itako.ibaraki.jp", {} }, + .{ "iwama.ibaraki.jp", {} }, + .{ "joso.ibaraki.jp", {} }, + .{ "kamisu.ibaraki.jp", {} }, + .{ "kasama.ibaraki.jp", {} }, + .{ "kashima.ibaraki.jp", {} }, + .{ "kasumigaura.ibaraki.jp", {} }, + .{ "koga.ibaraki.jp", {} }, + .{ "miho.ibaraki.jp", {} }, + .{ "mito.ibaraki.jp", {} }, + .{ "moriya.ibaraki.jp", {} }, + .{ "naka.ibaraki.jp", {} }, + .{ "namegata.ibaraki.jp", {} }, + .{ "oarai.ibaraki.jp", {} }, + .{ "ogawa.ibaraki.jp", {} }, + .{ "omitama.ibaraki.jp", {} }, + .{ "ryugasaki.ibaraki.jp", {} }, + .{ "sakai.ibaraki.jp", {} }, + .{ "sakuragawa.ibaraki.jp", {} }, + .{ "shimodate.ibaraki.jp", {} }, + .{ "shimotsuma.ibaraki.jp", {} }, + .{ "shirosato.ibaraki.jp", {} }, + .{ "sowa.ibaraki.jp", {} }, + .{ "suifu.ibaraki.jp", {} }, + .{ "takahagi.ibaraki.jp", {} }, + .{ "tamatsukuri.ibaraki.jp", {} }, + .{ "tokai.ibaraki.jp", {} }, + .{ "tomobe.ibaraki.jp", {} }, + .{ "tone.ibaraki.jp", {} }, + .{ "toride.ibaraki.jp", {} }, + .{ "tsuchiura.ibaraki.jp", {} }, + .{ "tsukuba.ibaraki.jp", {} }, + .{ "uchihara.ibaraki.jp", {} }, + .{ "ushiku.ibaraki.jp", {} }, + .{ "yachiyo.ibaraki.jp", {} }, + .{ "yamagata.ibaraki.jp", {} }, + .{ "yawara.ibaraki.jp", {} }, + .{ "yuki.ibaraki.jp", {} }, + .{ "anamizu.ishikawa.jp", {} }, + .{ "hakui.ishikawa.jp", {} }, + .{ "hakusan.ishikawa.jp", {} }, + .{ "kaga.ishikawa.jp", {} }, + .{ "kahoku.ishikawa.jp", {} }, + .{ "kanazawa.ishikawa.jp", {} }, + .{ "kawakita.ishikawa.jp", {} }, + .{ "komatsu.ishikawa.jp", {} }, + .{ "nakanoto.ishikawa.jp", {} }, + .{ "nanao.ishikawa.jp", {} }, + .{ "nomi.ishikawa.jp", {} }, + .{ "nonoichi.ishikawa.jp", {} }, + .{ "noto.ishikawa.jp", {} }, + .{ "shika.ishikawa.jp", {} }, + .{ "suzu.ishikawa.jp", {} }, + .{ "tsubata.ishikawa.jp", {} }, + .{ "tsurugi.ishikawa.jp", {} }, + .{ "uchinada.ishikawa.jp", {} }, + .{ "wajima.ishikawa.jp", {} }, + .{ "fudai.iwate.jp", {} }, + .{ "fujisawa.iwate.jp", {} }, + .{ "hanamaki.iwate.jp", {} }, + .{ "hiraizumi.iwate.jp", {} }, + .{ "hirono.iwate.jp", {} }, + .{ "ichinohe.iwate.jp", {} }, + .{ "ichinoseki.iwate.jp", {} }, + .{ "iwaizumi.iwate.jp", {} }, + .{ "iwate.iwate.jp", {} }, + .{ "joboji.iwate.jp", {} }, + .{ "kamaishi.iwate.jp", {} }, + .{ "kanegasaki.iwate.jp", {} }, + .{ "karumai.iwate.jp", {} }, + .{ "kawai.iwate.jp", {} }, + .{ "kitakami.iwate.jp", {} }, + .{ "kuji.iwate.jp", {} }, + .{ "kunohe.iwate.jp", {} }, + .{ "kuzumaki.iwate.jp", {} }, + .{ "miyako.iwate.jp", {} }, + .{ "mizusawa.iwate.jp", {} }, + .{ "morioka.iwate.jp", {} }, + .{ "ninohe.iwate.jp", {} }, + .{ "noda.iwate.jp", {} }, + .{ "ofunato.iwate.jp", {} }, + .{ "oshu.iwate.jp", {} }, + .{ "otsuchi.iwate.jp", {} }, + .{ "rikuzentakata.iwate.jp", {} }, + .{ "shiwa.iwate.jp", {} }, + .{ "shizukuishi.iwate.jp", {} }, + .{ "sumita.iwate.jp", {} }, + .{ "tanohata.iwate.jp", {} }, + .{ "tono.iwate.jp", {} }, + .{ "yahaba.iwate.jp", {} }, + .{ "yamada.iwate.jp", {} }, + .{ "ayagawa.kagawa.jp", {} }, + .{ "higashikagawa.kagawa.jp", {} }, + .{ "kanonji.kagawa.jp", {} }, + .{ "kotohira.kagawa.jp", {} }, + .{ "manno.kagawa.jp", {} }, + .{ "marugame.kagawa.jp", {} }, + .{ "mitoyo.kagawa.jp", {} }, + .{ "naoshima.kagawa.jp", {} }, + .{ "sanuki.kagawa.jp", {} }, + .{ "tadotsu.kagawa.jp", {} }, + .{ "takamatsu.kagawa.jp", {} }, + .{ "tonosho.kagawa.jp", {} }, + .{ "uchinomi.kagawa.jp", {} }, + .{ "utazu.kagawa.jp", {} }, + .{ "zentsuji.kagawa.jp", {} }, + .{ "akune.kagoshima.jp", {} }, + .{ "amami.kagoshima.jp", {} }, + .{ "hioki.kagoshima.jp", {} }, + .{ "isa.kagoshima.jp", {} }, + .{ "isen.kagoshima.jp", {} }, + .{ "izumi.kagoshima.jp", {} }, + .{ "kagoshima.kagoshima.jp", {} }, + .{ "kanoya.kagoshima.jp", {} }, + .{ "kawanabe.kagoshima.jp", {} }, + .{ "kinko.kagoshima.jp", {} }, + .{ "kouyama.kagoshima.jp", {} }, + .{ "makurazaki.kagoshima.jp", {} }, + .{ "matsumoto.kagoshima.jp", {} }, + .{ "minamitane.kagoshima.jp", {} }, + .{ "nakatane.kagoshima.jp", {} }, + .{ "nishinoomote.kagoshima.jp", {} }, + .{ "satsumasendai.kagoshima.jp", {} }, + .{ "soo.kagoshima.jp", {} }, + .{ "tarumizu.kagoshima.jp", {} }, + .{ "yusui.kagoshima.jp", {} }, + .{ "aikawa.kanagawa.jp", {} }, + .{ "atsugi.kanagawa.jp", {} }, + .{ "ayase.kanagawa.jp", {} }, + .{ "chigasaki.kanagawa.jp", {} }, + .{ "ebina.kanagawa.jp", {} }, + .{ "fujisawa.kanagawa.jp", {} }, + .{ "hadano.kanagawa.jp", {} }, + .{ "hakone.kanagawa.jp", {} }, + .{ "hiratsuka.kanagawa.jp", {} }, + .{ "isehara.kanagawa.jp", {} }, + .{ "kaisei.kanagawa.jp", {} }, + .{ "kamakura.kanagawa.jp", {} }, + .{ "kiyokawa.kanagawa.jp", {} }, + .{ "matsuda.kanagawa.jp", {} }, + .{ "minamiashigara.kanagawa.jp", {} }, + .{ "miura.kanagawa.jp", {} }, + .{ "nakai.kanagawa.jp", {} }, + .{ "ninomiya.kanagawa.jp", {} }, + .{ "odawara.kanagawa.jp", {} }, + .{ "oi.kanagawa.jp", {} }, + .{ "oiso.kanagawa.jp", {} }, + .{ "sagamihara.kanagawa.jp", {} }, + .{ "samukawa.kanagawa.jp", {} }, + .{ "tsukui.kanagawa.jp", {} }, + .{ "yamakita.kanagawa.jp", {} }, + .{ "yamato.kanagawa.jp", {} }, + .{ "yokosuka.kanagawa.jp", {} }, + .{ "yugawara.kanagawa.jp", {} }, + .{ "zama.kanagawa.jp", {} }, + .{ "zushi.kanagawa.jp", {} }, + .{ "aki.kochi.jp", {} }, + .{ "geisei.kochi.jp", {} }, + .{ "hidaka.kochi.jp", {} }, + .{ "higashitsuno.kochi.jp", {} }, + .{ "ino.kochi.jp", {} }, + .{ "kagami.kochi.jp", {} }, + .{ "kami.kochi.jp", {} }, + .{ "kitagawa.kochi.jp", {} }, + .{ "kochi.kochi.jp", {} }, + .{ "mihara.kochi.jp", {} }, + .{ "motoyama.kochi.jp", {} }, + .{ "muroto.kochi.jp", {} }, + .{ "nahari.kochi.jp", {} }, + .{ "nakamura.kochi.jp", {} }, + .{ "nankoku.kochi.jp", {} }, + .{ "nishitosa.kochi.jp", {} }, + .{ "niyodogawa.kochi.jp", {} }, + .{ "ochi.kochi.jp", {} }, + .{ "okawa.kochi.jp", {} }, + .{ "otoyo.kochi.jp", {} }, + .{ "otsuki.kochi.jp", {} }, + .{ "sakawa.kochi.jp", {} }, + .{ "sukumo.kochi.jp", {} }, + .{ "susaki.kochi.jp", {} }, + .{ "tosa.kochi.jp", {} }, + .{ "tosashimizu.kochi.jp", {} }, + .{ "toyo.kochi.jp", {} }, + .{ "tsuno.kochi.jp", {} }, + .{ "umaji.kochi.jp", {} }, + .{ "yasuda.kochi.jp", {} }, + .{ "yusuhara.kochi.jp", {} }, + .{ "amakusa.kumamoto.jp", {} }, + .{ "arao.kumamoto.jp", {} }, + .{ "aso.kumamoto.jp", {} }, + .{ "choyo.kumamoto.jp", {} }, + .{ "gyokuto.kumamoto.jp", {} }, + .{ "kamiamakusa.kumamoto.jp", {} }, + .{ "kikuchi.kumamoto.jp", {} }, + .{ "kumamoto.kumamoto.jp", {} }, + .{ "mashiki.kumamoto.jp", {} }, + .{ "mifune.kumamoto.jp", {} }, + .{ "minamata.kumamoto.jp", {} }, + .{ "minamioguni.kumamoto.jp", {} }, + .{ "nagasu.kumamoto.jp", {} }, + .{ "nishihara.kumamoto.jp", {} }, + .{ "oguni.kumamoto.jp", {} }, + .{ "ozu.kumamoto.jp", {} }, + .{ "sumoto.kumamoto.jp", {} }, + .{ "takamori.kumamoto.jp", {} }, + .{ "uki.kumamoto.jp", {} }, + .{ "uto.kumamoto.jp", {} }, + .{ "yamaga.kumamoto.jp", {} }, + .{ "yamato.kumamoto.jp", {} }, + .{ "yatsushiro.kumamoto.jp", {} }, + .{ "ayabe.kyoto.jp", {} }, + .{ "fukuchiyama.kyoto.jp", {} }, + .{ "higashiyama.kyoto.jp", {} }, + .{ "ide.kyoto.jp", {} }, + .{ "ine.kyoto.jp", {} }, + .{ "joyo.kyoto.jp", {} }, + .{ "kameoka.kyoto.jp", {} }, + .{ "kamo.kyoto.jp", {} }, + .{ "kita.kyoto.jp", {} }, + .{ "kizu.kyoto.jp", {} }, + .{ "kumiyama.kyoto.jp", {} }, + .{ "kyotamba.kyoto.jp", {} }, + .{ "kyotanabe.kyoto.jp", {} }, + .{ "kyotango.kyoto.jp", {} }, + .{ "maizuru.kyoto.jp", {} }, + .{ "minami.kyoto.jp", {} }, + .{ "minamiyamashiro.kyoto.jp", {} }, + .{ "miyazu.kyoto.jp", {} }, + .{ "muko.kyoto.jp", {} }, + .{ "nagaokakyo.kyoto.jp", {} }, + .{ "nakagyo.kyoto.jp", {} }, + .{ "nantan.kyoto.jp", {} }, + .{ "oyamazaki.kyoto.jp", {} }, + .{ "sakyo.kyoto.jp", {} }, + .{ "seika.kyoto.jp", {} }, + .{ "tanabe.kyoto.jp", {} }, + .{ "uji.kyoto.jp", {} }, + .{ "ujitawara.kyoto.jp", {} }, + .{ "wazuka.kyoto.jp", {} }, + .{ "yamashina.kyoto.jp", {} }, + .{ "yawata.kyoto.jp", {} }, + .{ "asahi.mie.jp", {} }, + .{ "inabe.mie.jp", {} }, + .{ "ise.mie.jp", {} }, + .{ "kameyama.mie.jp", {} }, + .{ "kawagoe.mie.jp", {} }, + .{ "kiho.mie.jp", {} }, + .{ "kisosaki.mie.jp", {} }, + .{ "kiwa.mie.jp", {} }, + .{ "komono.mie.jp", {} }, + .{ "kumano.mie.jp", {} }, + .{ "kuwana.mie.jp", {} }, + .{ "matsusaka.mie.jp", {} }, + .{ "meiwa.mie.jp", {} }, + .{ "mihama.mie.jp", {} }, + .{ "minamiise.mie.jp", {} }, + .{ "misugi.mie.jp", {} }, + .{ "miyama.mie.jp", {} }, + .{ "nabari.mie.jp", {} }, + .{ "shima.mie.jp", {} }, + .{ "suzuka.mie.jp", {} }, + .{ "tado.mie.jp", {} }, + .{ "taiki.mie.jp", {} }, + .{ "taki.mie.jp", {} }, + .{ "tamaki.mie.jp", {} }, + .{ "toba.mie.jp", {} }, + .{ "tsu.mie.jp", {} }, + .{ "udono.mie.jp", {} }, + .{ "ureshino.mie.jp", {} }, + .{ "watarai.mie.jp", {} }, + .{ "yokkaichi.mie.jp", {} }, + .{ "furukawa.miyagi.jp", {} }, + .{ "higashimatsushima.miyagi.jp", {} }, + .{ "ishinomaki.miyagi.jp", {} }, + .{ "iwanuma.miyagi.jp", {} }, + .{ "kakuda.miyagi.jp", {} }, + .{ "kami.miyagi.jp", {} }, + .{ "kawasaki.miyagi.jp", {} }, + .{ "marumori.miyagi.jp", {} }, + .{ "matsushima.miyagi.jp", {} }, + .{ "minamisanriku.miyagi.jp", {} }, + .{ "misato.miyagi.jp", {} }, + .{ "murata.miyagi.jp", {} }, + .{ "natori.miyagi.jp", {} }, + .{ "ogawara.miyagi.jp", {} }, + .{ "ohira.miyagi.jp", {} }, + .{ "onagawa.miyagi.jp", {} }, + .{ "osaki.miyagi.jp", {} }, + .{ "rifu.miyagi.jp", {} }, + .{ "semine.miyagi.jp", {} }, + .{ "shibata.miyagi.jp", {} }, + .{ "shichikashuku.miyagi.jp", {} }, + .{ "shikama.miyagi.jp", {} }, + .{ "shiogama.miyagi.jp", {} }, + .{ "shiroishi.miyagi.jp", {} }, + .{ "tagajo.miyagi.jp", {} }, + .{ "taiwa.miyagi.jp", {} }, + .{ "tome.miyagi.jp", {} }, + .{ "tomiya.miyagi.jp", {} }, + .{ "wakuya.miyagi.jp", {} }, + .{ "watari.miyagi.jp", {} }, + .{ "yamamoto.miyagi.jp", {} }, + .{ "zao.miyagi.jp", {} }, + .{ "aya.miyazaki.jp", {} }, + .{ "ebino.miyazaki.jp", {} }, + .{ "gokase.miyazaki.jp", {} }, + .{ "hyuga.miyazaki.jp", {} }, + .{ "kadogawa.miyazaki.jp", {} }, + .{ "kawaminami.miyazaki.jp", {} }, + .{ "kijo.miyazaki.jp", {} }, + .{ "kitagawa.miyazaki.jp", {} }, + .{ "kitakata.miyazaki.jp", {} }, + .{ "kitaura.miyazaki.jp", {} }, + .{ "kobayashi.miyazaki.jp", {} }, + .{ "kunitomi.miyazaki.jp", {} }, + .{ "kushima.miyazaki.jp", {} }, + .{ "mimata.miyazaki.jp", {} }, + .{ "miyakonojo.miyazaki.jp", {} }, + .{ "miyazaki.miyazaki.jp", {} }, + .{ "morotsuka.miyazaki.jp", {} }, + .{ "nichinan.miyazaki.jp", {} }, + .{ "nishimera.miyazaki.jp", {} }, + .{ "nobeoka.miyazaki.jp", {} }, + .{ "saito.miyazaki.jp", {} }, + .{ "shiiba.miyazaki.jp", {} }, + .{ "shintomi.miyazaki.jp", {} }, + .{ "takaharu.miyazaki.jp", {} }, + .{ "takanabe.miyazaki.jp", {} }, + .{ "takazaki.miyazaki.jp", {} }, + .{ "tsuno.miyazaki.jp", {} }, + .{ "achi.nagano.jp", {} }, + .{ "agematsu.nagano.jp", {} }, + .{ "anan.nagano.jp", {} }, + .{ "aoki.nagano.jp", {} }, + .{ "asahi.nagano.jp", {} }, + .{ "azumino.nagano.jp", {} }, + .{ "chikuhoku.nagano.jp", {} }, + .{ "chikuma.nagano.jp", {} }, + .{ "chino.nagano.jp", {} }, + .{ "fujimi.nagano.jp", {} }, + .{ "hakuba.nagano.jp", {} }, + .{ "hara.nagano.jp", {} }, + .{ "hiraya.nagano.jp", {} }, + .{ "iida.nagano.jp", {} }, + .{ "iijima.nagano.jp", {} }, + .{ "iiyama.nagano.jp", {} }, + .{ "iizuna.nagano.jp", {} }, + .{ "ikeda.nagano.jp", {} }, + .{ "ikusaka.nagano.jp", {} }, + .{ "ina.nagano.jp", {} }, + .{ "karuizawa.nagano.jp", {} }, + .{ "kawakami.nagano.jp", {} }, + .{ "kiso.nagano.jp", {} }, + .{ "kisofukushima.nagano.jp", {} }, + .{ "kitaaiki.nagano.jp", {} }, + .{ "komagane.nagano.jp", {} }, + .{ "komoro.nagano.jp", {} }, + .{ "matsukawa.nagano.jp", {} }, + .{ "matsumoto.nagano.jp", {} }, + .{ "miasa.nagano.jp", {} }, + .{ "minamiaiki.nagano.jp", {} }, + .{ "minamimaki.nagano.jp", {} }, + .{ "minamiminowa.nagano.jp", {} }, + .{ "minowa.nagano.jp", {} }, + .{ "miyada.nagano.jp", {} }, + .{ "miyota.nagano.jp", {} }, + .{ "mochizuki.nagano.jp", {} }, + .{ "nagano.nagano.jp", {} }, + .{ "nagawa.nagano.jp", {} }, + .{ "nagiso.nagano.jp", {} }, + .{ "nakagawa.nagano.jp", {} }, + .{ "nakano.nagano.jp", {} }, + .{ "nozawaonsen.nagano.jp", {} }, + .{ "obuse.nagano.jp", {} }, + .{ "ogawa.nagano.jp", {} }, + .{ "okaya.nagano.jp", {} }, + .{ "omachi.nagano.jp", {} }, + .{ "omi.nagano.jp", {} }, + .{ "ookuwa.nagano.jp", {} }, + .{ "ooshika.nagano.jp", {} }, + .{ "otaki.nagano.jp", {} }, + .{ "otari.nagano.jp", {} }, + .{ "sakae.nagano.jp", {} }, + .{ "sakaki.nagano.jp", {} }, + .{ "saku.nagano.jp", {} }, + .{ "sakuho.nagano.jp", {} }, + .{ "shimosuwa.nagano.jp", {} }, + .{ "shinanomachi.nagano.jp", {} }, + .{ "shiojiri.nagano.jp", {} }, + .{ "suwa.nagano.jp", {} }, + .{ "suzaka.nagano.jp", {} }, + .{ "takagi.nagano.jp", {} }, + .{ "takamori.nagano.jp", {} }, + .{ "takayama.nagano.jp", {} }, + .{ "tateshina.nagano.jp", {} }, + .{ "tatsuno.nagano.jp", {} }, + .{ "togakushi.nagano.jp", {} }, + .{ "togura.nagano.jp", {} }, + .{ "tomi.nagano.jp", {} }, + .{ "ueda.nagano.jp", {} }, + .{ "wada.nagano.jp", {} }, + .{ "yamagata.nagano.jp", {} }, + .{ "yamanouchi.nagano.jp", {} }, + .{ "yasaka.nagano.jp", {} }, + .{ "yasuoka.nagano.jp", {} }, + .{ "chijiwa.nagasaki.jp", {} }, + .{ "futsu.nagasaki.jp", {} }, + .{ "goto.nagasaki.jp", {} }, + .{ "hasami.nagasaki.jp", {} }, + .{ "hirado.nagasaki.jp", {} }, + .{ "iki.nagasaki.jp", {} }, + .{ "isahaya.nagasaki.jp", {} }, + .{ "kawatana.nagasaki.jp", {} }, + .{ "kuchinotsu.nagasaki.jp", {} }, + .{ "matsuura.nagasaki.jp", {} }, + .{ "nagasaki.nagasaki.jp", {} }, + .{ "obama.nagasaki.jp", {} }, + .{ "omura.nagasaki.jp", {} }, + .{ "oseto.nagasaki.jp", {} }, + .{ "saikai.nagasaki.jp", {} }, + .{ "sasebo.nagasaki.jp", {} }, + .{ "seihi.nagasaki.jp", {} }, + .{ "shimabara.nagasaki.jp", {} }, + .{ "shinkamigoto.nagasaki.jp", {} }, + .{ "togitsu.nagasaki.jp", {} }, + .{ "tsushima.nagasaki.jp", {} }, + .{ "unzen.nagasaki.jp", {} }, + .{ "ando.nara.jp", {} }, + .{ "gose.nara.jp", {} }, + .{ "heguri.nara.jp", {} }, + .{ "higashiyoshino.nara.jp", {} }, + .{ "ikaruga.nara.jp", {} }, + .{ "ikoma.nara.jp", {} }, + .{ "kamikitayama.nara.jp", {} }, + .{ "kanmaki.nara.jp", {} }, + .{ "kashiba.nara.jp", {} }, + .{ "kashihara.nara.jp", {} }, + .{ "katsuragi.nara.jp", {} }, + .{ "kawai.nara.jp", {} }, + .{ "kawakami.nara.jp", {} }, + .{ "kawanishi.nara.jp", {} }, + .{ "koryo.nara.jp", {} }, + .{ "kurotaki.nara.jp", {} }, + .{ "mitsue.nara.jp", {} }, + .{ "miyake.nara.jp", {} }, + .{ "nara.nara.jp", {} }, + .{ "nosegawa.nara.jp", {} }, + .{ "oji.nara.jp", {} }, + .{ "ouda.nara.jp", {} }, + .{ "oyodo.nara.jp", {} }, + .{ "sakurai.nara.jp", {} }, + .{ "sango.nara.jp", {} }, + .{ "shimoichi.nara.jp", {} }, + .{ "shimokitayama.nara.jp", {} }, + .{ "shinjo.nara.jp", {} }, + .{ "soni.nara.jp", {} }, + .{ "takatori.nara.jp", {} }, + .{ "tawaramoto.nara.jp", {} }, + .{ "tenkawa.nara.jp", {} }, + .{ "tenri.nara.jp", {} }, + .{ "uda.nara.jp", {} }, + .{ "yamatokoriyama.nara.jp", {} }, + .{ "yamatotakada.nara.jp", {} }, + .{ "yamazoe.nara.jp", {} }, + .{ "yoshino.nara.jp", {} }, + .{ "aga.niigata.jp", {} }, + .{ "agano.niigata.jp", {} }, + .{ "gosen.niigata.jp", {} }, + .{ "itoigawa.niigata.jp", {} }, + .{ "izumozaki.niigata.jp", {} }, + .{ "joetsu.niigata.jp", {} }, + .{ "kamo.niigata.jp", {} }, + .{ "kariwa.niigata.jp", {} }, + .{ "kashiwazaki.niigata.jp", {} }, + .{ "minamiuonuma.niigata.jp", {} }, + .{ "mitsuke.niigata.jp", {} }, + .{ "muika.niigata.jp", {} }, + .{ "murakami.niigata.jp", {} }, + .{ "myoko.niigata.jp", {} }, + .{ "nagaoka.niigata.jp", {} }, + .{ "niigata.niigata.jp", {} }, + .{ "ojiya.niigata.jp", {} }, + .{ "omi.niigata.jp", {} }, + .{ "sado.niigata.jp", {} }, + .{ "sanjo.niigata.jp", {} }, + .{ "seiro.niigata.jp", {} }, + .{ "seirou.niigata.jp", {} }, + .{ "sekikawa.niigata.jp", {} }, + .{ "shibata.niigata.jp", {} }, + .{ "tagami.niigata.jp", {} }, + .{ "tainai.niigata.jp", {} }, + .{ "tochio.niigata.jp", {} }, + .{ "tokamachi.niigata.jp", {} }, + .{ "tsubame.niigata.jp", {} }, + .{ "tsunan.niigata.jp", {} }, + .{ "uonuma.niigata.jp", {} }, + .{ "yahiko.niigata.jp", {} }, + .{ "yoita.niigata.jp", {} }, + .{ "yuzawa.niigata.jp", {} }, + .{ "beppu.oita.jp", {} }, + .{ "bungoono.oita.jp", {} }, + .{ "bungotakada.oita.jp", {} }, + .{ "hasama.oita.jp", {} }, + .{ "hiji.oita.jp", {} }, + .{ "himeshima.oita.jp", {} }, + .{ "hita.oita.jp", {} }, + .{ "kamitsue.oita.jp", {} }, + .{ "kokonoe.oita.jp", {} }, + .{ "kuju.oita.jp", {} }, + .{ "kunisaki.oita.jp", {} }, + .{ "kusu.oita.jp", {} }, + .{ "oita.oita.jp", {} }, + .{ "saiki.oita.jp", {} }, + .{ "taketa.oita.jp", {} }, + .{ "tsukumi.oita.jp", {} }, + .{ "usa.oita.jp", {} }, + .{ "usuki.oita.jp", {} }, + .{ "yufu.oita.jp", {} }, + .{ "akaiwa.okayama.jp", {} }, + .{ "asakuchi.okayama.jp", {} }, + .{ "bizen.okayama.jp", {} }, + .{ "hayashima.okayama.jp", {} }, + .{ "ibara.okayama.jp", {} }, + .{ "kagamino.okayama.jp", {} }, + .{ "kasaoka.okayama.jp", {} }, + .{ "kibichuo.okayama.jp", {} }, + .{ "kumenan.okayama.jp", {} }, + .{ "kurashiki.okayama.jp", {} }, + .{ "maniwa.okayama.jp", {} }, + .{ "misaki.okayama.jp", {} }, + .{ "nagi.okayama.jp", {} }, + .{ "niimi.okayama.jp", {} }, + .{ "nishiawakura.okayama.jp", {} }, + .{ "okayama.okayama.jp", {} }, + .{ "satosho.okayama.jp", {} }, + .{ "setouchi.okayama.jp", {} }, + .{ "shinjo.okayama.jp", {} }, + .{ "shoo.okayama.jp", {} }, + .{ "soja.okayama.jp", {} }, + .{ "takahashi.okayama.jp", {} }, + .{ "tamano.okayama.jp", {} }, + .{ "tsuyama.okayama.jp", {} }, + .{ "wake.okayama.jp", {} }, + .{ "yakage.okayama.jp", {} }, + .{ "aguni.okinawa.jp", {} }, + .{ "ginowan.okinawa.jp", {} }, + .{ "ginoza.okinawa.jp", {} }, + .{ "gushikami.okinawa.jp", {} }, + .{ "haebaru.okinawa.jp", {} }, + .{ "higashi.okinawa.jp", {} }, + .{ "hirara.okinawa.jp", {} }, + .{ "iheya.okinawa.jp", {} }, + .{ "ishigaki.okinawa.jp", {} }, + .{ "ishikawa.okinawa.jp", {} }, + .{ "itoman.okinawa.jp", {} }, + .{ "izena.okinawa.jp", {} }, + .{ "kadena.okinawa.jp", {} }, + .{ "kin.okinawa.jp", {} }, + .{ "kitadaito.okinawa.jp", {} }, + .{ "kitanakagusuku.okinawa.jp", {} }, + .{ "kumejima.okinawa.jp", {} }, + .{ "kunigami.okinawa.jp", {} }, + .{ "minamidaito.okinawa.jp", {} }, + .{ "motobu.okinawa.jp", {} }, + .{ "nago.okinawa.jp", {} }, + .{ "naha.okinawa.jp", {} }, + .{ "nakagusuku.okinawa.jp", {} }, + .{ "nakijin.okinawa.jp", {} }, + .{ "nanjo.okinawa.jp", {} }, + .{ "nishihara.okinawa.jp", {} }, + .{ "ogimi.okinawa.jp", {} }, + .{ "okinawa.okinawa.jp", {} }, + .{ "onna.okinawa.jp", {} }, + .{ "shimoji.okinawa.jp", {} }, + .{ "taketomi.okinawa.jp", {} }, + .{ "tarama.okinawa.jp", {} }, + .{ "tokashiki.okinawa.jp", {} }, + .{ "tomigusuku.okinawa.jp", {} }, + .{ "tonaki.okinawa.jp", {} }, + .{ "urasoe.okinawa.jp", {} }, + .{ "uruma.okinawa.jp", {} }, + .{ "yaese.okinawa.jp", {} }, + .{ "yomitan.okinawa.jp", {} }, + .{ "yonabaru.okinawa.jp", {} }, + .{ "yonaguni.okinawa.jp", {} }, + .{ "zamami.okinawa.jp", {} }, + .{ "abeno.osaka.jp", {} }, + .{ "chihayaakasaka.osaka.jp", {} }, + .{ "chuo.osaka.jp", {} }, + .{ "daito.osaka.jp", {} }, + .{ "fujiidera.osaka.jp", {} }, + .{ "habikino.osaka.jp", {} }, + .{ "hannan.osaka.jp", {} }, + .{ "higashiosaka.osaka.jp", {} }, + .{ "higashisumiyoshi.osaka.jp", {} }, + .{ "higashiyodogawa.osaka.jp", {} }, + .{ "hirakata.osaka.jp", {} }, + .{ "ibaraki.osaka.jp", {} }, + .{ "ikeda.osaka.jp", {} }, + .{ "izumi.osaka.jp", {} }, + .{ "izumiotsu.osaka.jp", {} }, + .{ "izumisano.osaka.jp", {} }, + .{ "kadoma.osaka.jp", {} }, + .{ "kaizuka.osaka.jp", {} }, + .{ "kanan.osaka.jp", {} }, + .{ "kashiwara.osaka.jp", {} }, + .{ "katano.osaka.jp", {} }, + .{ "kawachinagano.osaka.jp", {} }, + .{ "kishiwada.osaka.jp", {} }, + .{ "kita.osaka.jp", {} }, + .{ "kumatori.osaka.jp", {} }, + .{ "matsubara.osaka.jp", {} }, + .{ "minato.osaka.jp", {} }, + .{ "minoh.osaka.jp", {} }, + .{ "misaki.osaka.jp", {} }, + .{ "moriguchi.osaka.jp", {} }, + .{ "neyagawa.osaka.jp", {} }, + .{ "nishi.osaka.jp", {} }, + .{ "nose.osaka.jp", {} }, + .{ "osakasayama.osaka.jp", {} }, + .{ "sakai.osaka.jp", {} }, + .{ "sayama.osaka.jp", {} }, + .{ "sennan.osaka.jp", {} }, + .{ "settsu.osaka.jp", {} }, + .{ "shijonawate.osaka.jp", {} }, + .{ "shimamoto.osaka.jp", {} }, + .{ "suita.osaka.jp", {} }, + .{ "tadaoka.osaka.jp", {} }, + .{ "taishi.osaka.jp", {} }, + .{ "tajiri.osaka.jp", {} }, + .{ "takaishi.osaka.jp", {} }, + .{ "takatsuki.osaka.jp", {} }, + .{ "tondabayashi.osaka.jp", {} }, + .{ "toyonaka.osaka.jp", {} }, + .{ "toyono.osaka.jp", {} }, + .{ "yao.osaka.jp", {} }, + .{ "ariake.saga.jp", {} }, + .{ "arita.saga.jp", {} }, + .{ "fukudomi.saga.jp", {} }, + .{ "genkai.saga.jp", {} }, + .{ "hamatama.saga.jp", {} }, + .{ "hizen.saga.jp", {} }, + .{ "imari.saga.jp", {} }, + .{ "kamimine.saga.jp", {} }, + .{ "kanzaki.saga.jp", {} }, + .{ "karatsu.saga.jp", {} }, + .{ "kashima.saga.jp", {} }, + .{ "kitagata.saga.jp", {} }, + .{ "kitahata.saga.jp", {} }, + .{ "kiyama.saga.jp", {} }, + .{ "kouhoku.saga.jp", {} }, + .{ "kyuragi.saga.jp", {} }, + .{ "nishiarita.saga.jp", {} }, + .{ "ogi.saga.jp", {} }, + .{ "omachi.saga.jp", {} }, + .{ "ouchi.saga.jp", {} }, + .{ "saga.saga.jp", {} }, + .{ "shiroishi.saga.jp", {} }, + .{ "taku.saga.jp", {} }, + .{ "tara.saga.jp", {} }, + .{ "tosu.saga.jp", {} }, + .{ "yoshinogari.saga.jp", {} }, + .{ "arakawa.saitama.jp", {} }, + .{ "asaka.saitama.jp", {} }, + .{ "chichibu.saitama.jp", {} }, + .{ "fujimi.saitama.jp", {} }, + .{ "fujimino.saitama.jp", {} }, + .{ "fukaya.saitama.jp", {} }, + .{ "hanno.saitama.jp", {} }, + .{ "hanyu.saitama.jp", {} }, + .{ "hasuda.saitama.jp", {} }, + .{ "hatogaya.saitama.jp", {} }, + .{ "hatoyama.saitama.jp", {} }, + .{ "hidaka.saitama.jp", {} }, + .{ "higashichichibu.saitama.jp", {} }, + .{ "higashimatsuyama.saitama.jp", {} }, + .{ "honjo.saitama.jp", {} }, + .{ "ina.saitama.jp", {} }, + .{ "iruma.saitama.jp", {} }, + .{ "iwatsuki.saitama.jp", {} }, + .{ "kamiizumi.saitama.jp", {} }, + .{ "kamikawa.saitama.jp", {} }, + .{ "kamisato.saitama.jp", {} }, + .{ "kasukabe.saitama.jp", {} }, + .{ "kawagoe.saitama.jp", {} }, + .{ "kawaguchi.saitama.jp", {} }, + .{ "kawajima.saitama.jp", {} }, + .{ "kazo.saitama.jp", {} }, + .{ "kitamoto.saitama.jp", {} }, + .{ "koshigaya.saitama.jp", {} }, + .{ "kounosu.saitama.jp", {} }, + .{ "kuki.saitama.jp", {} }, + .{ "kumagaya.saitama.jp", {} }, + .{ "matsubushi.saitama.jp", {} }, + .{ "minano.saitama.jp", {} }, + .{ "misato.saitama.jp", {} }, + .{ "miyashiro.saitama.jp", {} }, + .{ "miyoshi.saitama.jp", {} }, + .{ "moroyama.saitama.jp", {} }, + .{ "nagatoro.saitama.jp", {} }, + .{ "namegawa.saitama.jp", {} }, + .{ "niiza.saitama.jp", {} }, + .{ "ogano.saitama.jp", {} }, + .{ "ogawa.saitama.jp", {} }, + .{ "ogose.saitama.jp", {} }, + .{ "okegawa.saitama.jp", {} }, + .{ "omiya.saitama.jp", {} }, + .{ "otaki.saitama.jp", {} }, + .{ "ranzan.saitama.jp", {} }, + .{ "ryokami.saitama.jp", {} }, + .{ "saitama.saitama.jp", {} }, + .{ "sakado.saitama.jp", {} }, + .{ "satte.saitama.jp", {} }, + .{ "sayama.saitama.jp", {} }, + .{ "shiki.saitama.jp", {} }, + .{ "shiraoka.saitama.jp", {} }, + .{ "soka.saitama.jp", {} }, + .{ "sugito.saitama.jp", {} }, + .{ "toda.saitama.jp", {} }, + .{ "tokigawa.saitama.jp", {} }, + .{ "tokorozawa.saitama.jp", {} }, + .{ "tsurugashima.saitama.jp", {} }, + .{ "urawa.saitama.jp", {} }, + .{ "warabi.saitama.jp", {} }, + .{ "yashio.saitama.jp", {} }, + .{ "yokoze.saitama.jp", {} }, + .{ "yono.saitama.jp", {} }, + .{ "yorii.saitama.jp", {} }, + .{ "yoshida.saitama.jp", {} }, + .{ "yoshikawa.saitama.jp", {} }, + .{ "yoshimi.saitama.jp", {} }, + .{ "aisho.shiga.jp", {} }, + .{ "gamo.shiga.jp", {} }, + .{ "higashiomi.shiga.jp", {} }, + .{ "hikone.shiga.jp", {} }, + .{ "koka.shiga.jp", {} }, + .{ "konan.shiga.jp", {} }, + .{ "kosei.shiga.jp", {} }, + .{ "koto.shiga.jp", {} }, + .{ "kusatsu.shiga.jp", {} }, + .{ "maibara.shiga.jp", {} }, + .{ "moriyama.shiga.jp", {} }, + .{ "nagahama.shiga.jp", {} }, + .{ "nishiazai.shiga.jp", {} }, + .{ "notogawa.shiga.jp", {} }, + .{ "omihachiman.shiga.jp", {} }, + .{ "otsu.shiga.jp", {} }, + .{ "ritto.shiga.jp", {} }, + .{ "ryuoh.shiga.jp", {} }, + .{ "takashima.shiga.jp", {} }, + .{ "takatsuki.shiga.jp", {} }, + .{ "torahime.shiga.jp", {} }, + .{ "toyosato.shiga.jp", {} }, + .{ "yasu.shiga.jp", {} }, + .{ "akagi.shimane.jp", {} }, + .{ "ama.shimane.jp", {} }, + .{ "gotsu.shimane.jp", {} }, + .{ "hamada.shimane.jp", {} }, + .{ "higashiizumo.shimane.jp", {} }, + .{ "hikawa.shimane.jp", {} }, + .{ "hikimi.shimane.jp", {} }, + .{ "izumo.shimane.jp", {} }, + .{ "kakinoki.shimane.jp", {} }, + .{ "masuda.shimane.jp", {} }, + .{ "matsue.shimane.jp", {} }, + .{ "misato.shimane.jp", {} }, + .{ "nishinoshima.shimane.jp", {} }, + .{ "ohda.shimane.jp", {} }, + .{ "okinoshima.shimane.jp", {} }, + .{ "okuizumo.shimane.jp", {} }, + .{ "shimane.shimane.jp", {} }, + .{ "tamayu.shimane.jp", {} }, + .{ "tsuwano.shimane.jp", {} }, + .{ "unnan.shimane.jp", {} }, + .{ "yakumo.shimane.jp", {} }, + .{ "yasugi.shimane.jp", {} }, + .{ "yatsuka.shimane.jp", {} }, + .{ "arai.shizuoka.jp", {} }, + .{ "atami.shizuoka.jp", {} }, + .{ "fuji.shizuoka.jp", {} }, + .{ "fujieda.shizuoka.jp", {} }, + .{ "fujikawa.shizuoka.jp", {} }, + .{ "fujinomiya.shizuoka.jp", {} }, + .{ "fukuroi.shizuoka.jp", {} }, + .{ "gotemba.shizuoka.jp", {} }, + .{ "haibara.shizuoka.jp", {} }, + .{ "hamamatsu.shizuoka.jp", {} }, + .{ "higashiizu.shizuoka.jp", {} }, + .{ "ito.shizuoka.jp", {} }, + .{ "iwata.shizuoka.jp", {} }, + .{ "izu.shizuoka.jp", {} }, + .{ "izunokuni.shizuoka.jp", {} }, + .{ "kakegawa.shizuoka.jp", {} }, + .{ "kannami.shizuoka.jp", {} }, + .{ "kawanehon.shizuoka.jp", {} }, + .{ "kawazu.shizuoka.jp", {} }, + .{ "kikugawa.shizuoka.jp", {} }, + .{ "kosai.shizuoka.jp", {} }, + .{ "makinohara.shizuoka.jp", {} }, + .{ "matsuzaki.shizuoka.jp", {} }, + .{ "minamiizu.shizuoka.jp", {} }, + .{ "mishima.shizuoka.jp", {} }, + .{ "morimachi.shizuoka.jp", {} }, + .{ "nishiizu.shizuoka.jp", {} }, + .{ "numazu.shizuoka.jp", {} }, + .{ "omaezaki.shizuoka.jp", {} }, + .{ "shimada.shizuoka.jp", {} }, + .{ "shimizu.shizuoka.jp", {} }, + .{ "shimoda.shizuoka.jp", {} }, + .{ "shizuoka.shizuoka.jp", {} }, + .{ "susono.shizuoka.jp", {} }, + .{ "yaizu.shizuoka.jp", {} }, + .{ "yoshida.shizuoka.jp", {} }, + .{ "ashikaga.tochigi.jp", {} }, + .{ "bato.tochigi.jp", {} }, + .{ "haga.tochigi.jp", {} }, + .{ "ichikai.tochigi.jp", {} }, + .{ "iwafune.tochigi.jp", {} }, + .{ "kaminokawa.tochigi.jp", {} }, + .{ "kanuma.tochigi.jp", {} }, + .{ "karasuyama.tochigi.jp", {} }, + .{ "kuroiso.tochigi.jp", {} }, + .{ "mashiko.tochigi.jp", {} }, + .{ "mibu.tochigi.jp", {} }, + .{ "moka.tochigi.jp", {} }, + .{ "motegi.tochigi.jp", {} }, + .{ "nasu.tochigi.jp", {} }, + .{ "nasushiobara.tochigi.jp", {} }, + .{ "nikko.tochigi.jp", {} }, + .{ "nishikata.tochigi.jp", {} }, + .{ "nogi.tochigi.jp", {} }, + .{ "ohira.tochigi.jp", {} }, + .{ "ohtawara.tochigi.jp", {} }, + .{ "oyama.tochigi.jp", {} }, + .{ "sakura.tochigi.jp", {} }, + .{ "sano.tochigi.jp", {} }, + .{ "shimotsuke.tochigi.jp", {} }, + .{ "shioya.tochigi.jp", {} }, + .{ "takanezawa.tochigi.jp", {} }, + .{ "tochigi.tochigi.jp", {} }, + .{ "tsuga.tochigi.jp", {} }, + .{ "ujiie.tochigi.jp", {} }, + .{ "utsunomiya.tochigi.jp", {} }, + .{ "yaita.tochigi.jp", {} }, + .{ "aizumi.tokushima.jp", {} }, + .{ "anan.tokushima.jp", {} }, + .{ "ichiba.tokushima.jp", {} }, + .{ "itano.tokushima.jp", {} }, + .{ "kainan.tokushima.jp", {} }, + .{ "komatsushima.tokushima.jp", {} }, + .{ "matsushige.tokushima.jp", {} }, + .{ "mima.tokushima.jp", {} }, + .{ "minami.tokushima.jp", {} }, + .{ "miyoshi.tokushima.jp", {} }, + .{ "mugi.tokushima.jp", {} }, + .{ "nakagawa.tokushima.jp", {} }, + .{ "naruto.tokushima.jp", {} }, + .{ "sanagochi.tokushima.jp", {} }, + .{ "shishikui.tokushima.jp", {} }, + .{ "tokushima.tokushima.jp", {} }, + .{ "wajiki.tokushima.jp", {} }, + .{ "adachi.tokyo.jp", {} }, + .{ "akiruno.tokyo.jp", {} }, + .{ "akishima.tokyo.jp", {} }, + .{ "aogashima.tokyo.jp", {} }, + .{ "arakawa.tokyo.jp", {} }, + .{ "bunkyo.tokyo.jp", {} }, + .{ "chiyoda.tokyo.jp", {} }, + .{ "chofu.tokyo.jp", {} }, + .{ "chuo.tokyo.jp", {} }, + .{ "edogawa.tokyo.jp", {} }, + .{ "fuchu.tokyo.jp", {} }, + .{ "fussa.tokyo.jp", {} }, + .{ "hachijo.tokyo.jp", {} }, + .{ "hachioji.tokyo.jp", {} }, + .{ "hamura.tokyo.jp", {} }, + .{ "higashikurume.tokyo.jp", {} }, + .{ "higashimurayama.tokyo.jp", {} }, + .{ "higashiyamato.tokyo.jp", {} }, + .{ "hino.tokyo.jp", {} }, + .{ "hinode.tokyo.jp", {} }, + .{ "hinohara.tokyo.jp", {} }, + .{ "inagi.tokyo.jp", {} }, + .{ "itabashi.tokyo.jp", {} }, + .{ "katsushika.tokyo.jp", {} }, + .{ "kita.tokyo.jp", {} }, + .{ "kiyose.tokyo.jp", {} }, + .{ "kodaira.tokyo.jp", {} }, + .{ "koganei.tokyo.jp", {} }, + .{ "kokubunji.tokyo.jp", {} }, + .{ "komae.tokyo.jp", {} }, + .{ "koto.tokyo.jp", {} }, + .{ "kouzushima.tokyo.jp", {} }, + .{ "kunitachi.tokyo.jp", {} }, + .{ "machida.tokyo.jp", {} }, + .{ "meguro.tokyo.jp", {} }, + .{ "minato.tokyo.jp", {} }, + .{ "mitaka.tokyo.jp", {} }, + .{ "mizuho.tokyo.jp", {} }, + .{ "musashimurayama.tokyo.jp", {} }, + .{ "musashino.tokyo.jp", {} }, + .{ "nakano.tokyo.jp", {} }, + .{ "nerima.tokyo.jp", {} }, + .{ "ogasawara.tokyo.jp", {} }, + .{ "okutama.tokyo.jp", {} }, + .{ "ome.tokyo.jp", {} }, + .{ "oshima.tokyo.jp", {} }, + .{ "ota.tokyo.jp", {} }, + .{ "setagaya.tokyo.jp", {} }, + .{ "shibuya.tokyo.jp", {} }, + .{ "shinagawa.tokyo.jp", {} }, + .{ "shinjuku.tokyo.jp", {} }, + .{ "suginami.tokyo.jp", {} }, + .{ "sumida.tokyo.jp", {} }, + .{ "tachikawa.tokyo.jp", {} }, + .{ "taito.tokyo.jp", {} }, + .{ "tama.tokyo.jp", {} }, + .{ "toshima.tokyo.jp", {} }, + .{ "chizu.tottori.jp", {} }, + .{ "hino.tottori.jp", {} }, + .{ "kawahara.tottori.jp", {} }, + .{ "koge.tottori.jp", {} }, + .{ "kotoura.tottori.jp", {} }, + .{ "misasa.tottori.jp", {} }, + .{ "nanbu.tottori.jp", {} }, + .{ "nichinan.tottori.jp", {} }, + .{ "sakaiminato.tottori.jp", {} }, + .{ "tottori.tottori.jp", {} }, + .{ "wakasa.tottori.jp", {} }, + .{ "yazu.tottori.jp", {} }, + .{ "yonago.tottori.jp", {} }, + .{ "asahi.toyama.jp", {} }, + .{ "fuchu.toyama.jp", {} }, + .{ "fukumitsu.toyama.jp", {} }, + .{ "funahashi.toyama.jp", {} }, + .{ "himi.toyama.jp", {} }, + .{ "imizu.toyama.jp", {} }, + .{ "inami.toyama.jp", {} }, + .{ "johana.toyama.jp", {} }, + .{ "kamiichi.toyama.jp", {} }, + .{ "kurobe.toyama.jp", {} }, + .{ "nakaniikawa.toyama.jp", {} }, + .{ "namerikawa.toyama.jp", {} }, + .{ "nanto.toyama.jp", {} }, + .{ "nyuzen.toyama.jp", {} }, + .{ "oyabe.toyama.jp", {} }, + .{ "taira.toyama.jp", {} }, + .{ "takaoka.toyama.jp", {} }, + .{ "tateyama.toyama.jp", {} }, + .{ "toga.toyama.jp", {} }, + .{ "tonami.toyama.jp", {} }, + .{ "toyama.toyama.jp", {} }, + .{ "unazuki.toyama.jp", {} }, + .{ "uozu.toyama.jp", {} }, + .{ "yamada.toyama.jp", {} }, + .{ "arida.wakayama.jp", {} }, + .{ "aridagawa.wakayama.jp", {} }, + .{ "gobo.wakayama.jp", {} }, + .{ "hashimoto.wakayama.jp", {} }, + .{ "hidaka.wakayama.jp", {} }, + .{ "hirogawa.wakayama.jp", {} }, + .{ "inami.wakayama.jp", {} }, + .{ "iwade.wakayama.jp", {} }, + .{ "kainan.wakayama.jp", {} }, + .{ "kamitonda.wakayama.jp", {} }, + .{ "katsuragi.wakayama.jp", {} }, + .{ "kimino.wakayama.jp", {} }, + .{ "kinokawa.wakayama.jp", {} }, + .{ "kitayama.wakayama.jp", {} }, + .{ "koya.wakayama.jp", {} }, + .{ "koza.wakayama.jp", {} }, + .{ "kozagawa.wakayama.jp", {} }, + .{ "kudoyama.wakayama.jp", {} }, + .{ "kushimoto.wakayama.jp", {} }, + .{ "mihama.wakayama.jp", {} }, + .{ "misato.wakayama.jp", {} }, + .{ "nachikatsuura.wakayama.jp", {} }, + .{ "shingu.wakayama.jp", {} }, + .{ "shirahama.wakayama.jp", {} }, + .{ "taiji.wakayama.jp", {} }, + .{ "tanabe.wakayama.jp", {} }, + .{ "wakayama.wakayama.jp", {} }, + .{ "yuasa.wakayama.jp", {} }, + .{ "yura.wakayama.jp", {} }, + .{ "asahi.yamagata.jp", {} }, + .{ "funagata.yamagata.jp", {} }, + .{ "higashine.yamagata.jp", {} }, + .{ "iide.yamagata.jp", {} }, + .{ "kahoku.yamagata.jp", {} }, + .{ "kaminoyama.yamagata.jp", {} }, + .{ "kaneyama.yamagata.jp", {} }, + .{ "kawanishi.yamagata.jp", {} }, + .{ "mamurogawa.yamagata.jp", {} }, + .{ "mikawa.yamagata.jp", {} }, + .{ "murayama.yamagata.jp", {} }, + .{ "nagai.yamagata.jp", {} }, + .{ "nakayama.yamagata.jp", {} }, + .{ "nanyo.yamagata.jp", {} }, + .{ "nishikawa.yamagata.jp", {} }, + .{ "obanazawa.yamagata.jp", {} }, + .{ "oe.yamagata.jp", {} }, + .{ "oguni.yamagata.jp", {} }, + .{ "ohkura.yamagata.jp", {} }, + .{ "oishida.yamagata.jp", {} }, + .{ "sagae.yamagata.jp", {} }, + .{ "sakata.yamagata.jp", {} }, + .{ "sakegawa.yamagata.jp", {} }, + .{ "shinjo.yamagata.jp", {} }, + .{ "shirataka.yamagata.jp", {} }, + .{ "shonai.yamagata.jp", {} }, + .{ "takahata.yamagata.jp", {} }, + .{ "tendo.yamagata.jp", {} }, + .{ "tozawa.yamagata.jp", {} }, + .{ "tsuruoka.yamagata.jp", {} }, + .{ "yamagata.yamagata.jp", {} }, + .{ "yamanobe.yamagata.jp", {} }, + .{ "yonezawa.yamagata.jp", {} }, + .{ "yuza.yamagata.jp", {} }, + .{ "abu.yamaguchi.jp", {} }, + .{ "hagi.yamaguchi.jp", {} }, + .{ "hikari.yamaguchi.jp", {} }, + .{ "hofu.yamaguchi.jp", {} }, + .{ "iwakuni.yamaguchi.jp", {} }, + .{ "kudamatsu.yamaguchi.jp", {} }, + .{ "mitou.yamaguchi.jp", {} }, + .{ "nagato.yamaguchi.jp", {} }, + .{ "oshima.yamaguchi.jp", {} }, + .{ "shimonoseki.yamaguchi.jp", {} }, + .{ "shunan.yamaguchi.jp", {} }, + .{ "tabuse.yamaguchi.jp", {} }, + .{ "tokuyama.yamaguchi.jp", {} }, + .{ "toyota.yamaguchi.jp", {} }, + .{ "ube.yamaguchi.jp", {} }, + .{ "yuu.yamaguchi.jp", {} }, + .{ "chuo.yamanashi.jp", {} }, + .{ "doshi.yamanashi.jp", {} }, + .{ "fuefuki.yamanashi.jp", {} }, + .{ "fujikawa.yamanashi.jp", {} }, + .{ "fujikawaguchiko.yamanashi.jp", {} }, + .{ "fujiyoshida.yamanashi.jp", {} }, + .{ "hayakawa.yamanashi.jp", {} }, + .{ "hokuto.yamanashi.jp", {} }, + .{ "ichikawamisato.yamanashi.jp", {} }, + .{ "kai.yamanashi.jp", {} }, + .{ "kofu.yamanashi.jp", {} }, + .{ "koshu.yamanashi.jp", {} }, + .{ "kosuge.yamanashi.jp", {} }, + .{ "minami-alps.yamanashi.jp", {} }, + .{ "minobu.yamanashi.jp", {} }, + .{ "nakamichi.yamanashi.jp", {} }, + .{ "nanbu.yamanashi.jp", {} }, + .{ "narusawa.yamanashi.jp", {} }, + .{ "nirasaki.yamanashi.jp", {} }, + .{ "nishikatsura.yamanashi.jp", {} }, + .{ "oshino.yamanashi.jp", {} }, + .{ "otsuki.yamanashi.jp", {} }, + .{ "showa.yamanashi.jp", {} }, + .{ "tabayama.yamanashi.jp", {} }, + .{ "tsuru.yamanashi.jp", {} }, + .{ "uenohara.yamanashi.jp", {} }, + .{ "yamanakako.yamanashi.jp", {} }, + .{ "yamanashi.yamanashi.jp", {} }, + .{ "ke", {} }, + .{ "ac.ke", {} }, + .{ "co.ke", {} }, + .{ "go.ke", {} }, + .{ "info.ke", {} }, + .{ "me.ke", {} }, + .{ "mobi.ke", {} }, + .{ "ne.ke", {} }, + .{ "or.ke", {} }, + .{ "sc.ke", {} }, + .{ "kg", {} }, + .{ "com.kg", {} }, + .{ "edu.kg", {} }, + .{ "gov.kg", {} }, + .{ "mil.kg", {} }, + .{ "net.kg", {} }, + .{ "org.kg", {} }, + .{ "*.kh", {} }, + .{ "ki", {} }, + .{ "biz.ki", {} }, + .{ "com.ki", {} }, + .{ "edu.ki", {} }, + .{ "gov.ki", {} }, + .{ "info.ki", {} }, + .{ "net.ki", {} }, + .{ "org.ki", {} }, + .{ "km", {} }, + .{ "ass.km", {} }, + .{ "com.km", {} }, + .{ "edu.km", {} }, + .{ "gov.km", {} }, + .{ "mil.km", {} }, + .{ "nom.km", {} }, + .{ "org.km", {} }, + .{ "prd.km", {} }, + .{ "tm.km", {} }, + .{ "asso.km", {} }, + .{ "coop.km", {} }, + .{ "gouv.km", {} }, + .{ "medecin.km", {} }, + .{ "notaires.km", {} }, + .{ "pharmaciens.km", {} }, + .{ "presse.km", {} }, + .{ "veterinaire.km", {} }, + .{ "kn", {} }, + .{ "edu.kn", {} }, + .{ "gov.kn", {} }, + .{ "net.kn", {} }, + .{ "org.kn", {} }, + .{ "kp", {} }, + .{ "com.kp", {} }, + .{ "edu.kp", {} }, + .{ "gov.kp", {} }, + .{ "org.kp", {} }, + .{ "rep.kp", {} }, + .{ "tra.kp", {} }, + .{ "kr", {} }, + .{ "ac.kr", {} }, + .{ "co.kr", {} }, + .{ "es.kr", {} }, + .{ "go.kr", {} }, + .{ "hs.kr", {} }, + .{ "kg.kr", {} }, + .{ "mil.kr", {} }, + .{ "ms.kr", {} }, + .{ "ne.kr", {} }, + .{ "or.kr", {} }, + .{ "pe.kr", {} }, + .{ "re.kr", {} }, + .{ "sc.kr", {} }, + .{ "busan.kr", {} }, + .{ "chungbuk.kr", {} }, + .{ "chungnam.kr", {} }, + .{ "daegu.kr", {} }, + .{ "daejeon.kr", {} }, + .{ "gangwon.kr", {} }, + .{ "gwangju.kr", {} }, + .{ "gyeongbuk.kr", {} }, + .{ "gyeonggi.kr", {} }, + .{ "gyeongnam.kr", {} }, + .{ "incheon.kr", {} }, + .{ "jeju.kr", {} }, + .{ "jeonbuk.kr", {} }, + .{ "jeonnam.kr", {} }, + .{ "seoul.kr", {} }, + .{ "ulsan.kr", {} }, + .{ "kw", {} }, + .{ "com.kw", {} }, + .{ "edu.kw", {} }, + .{ "emb.kw", {} }, + .{ "gov.kw", {} }, + .{ "ind.kw", {} }, + .{ "net.kw", {} }, + .{ "org.kw", {} }, + .{ "ky", {} }, + .{ "com.ky", {} }, + .{ "edu.ky", {} }, + .{ "net.ky", {} }, + .{ "org.ky", {} }, + .{ "kz", {} }, + .{ "com.kz", {} }, + .{ "edu.kz", {} }, + .{ "gov.kz", {} }, + .{ "mil.kz", {} }, + .{ "net.kz", {} }, + .{ "org.kz", {} }, + .{ "la", {} }, + .{ "com.la", {} }, + .{ "edu.la", {} }, + .{ "gov.la", {} }, + .{ "info.la", {} }, + .{ "int.la", {} }, + .{ "net.la", {} }, + .{ "org.la", {} }, + .{ "per.la", {} }, + .{ "lb", {} }, + .{ "com.lb", {} }, + .{ "edu.lb", {} }, + .{ "gov.lb", {} }, + .{ "net.lb", {} }, + .{ "org.lb", {} }, + .{ "lc", {} }, + .{ "co.lc", {} }, + .{ "com.lc", {} }, + .{ "edu.lc", {} }, + .{ "gov.lc", {} }, + .{ "net.lc", {} }, + .{ "org.lc", {} }, + .{ "li", {} }, + .{ "lk", {} }, + .{ "ac.lk", {} }, + .{ "assn.lk", {} }, + .{ "com.lk", {} }, + .{ "edu.lk", {} }, + .{ "gov.lk", {} }, + .{ "grp.lk", {} }, + .{ "hotel.lk", {} }, + .{ "int.lk", {} }, + .{ "ltd.lk", {} }, + .{ "net.lk", {} }, + .{ "ngo.lk", {} }, + .{ "org.lk", {} }, + .{ "sch.lk", {} }, + .{ "soc.lk", {} }, + .{ "web.lk", {} }, + .{ "lr", {} }, + .{ "com.lr", {} }, + .{ "edu.lr", {} }, + .{ "gov.lr", {} }, + .{ "net.lr", {} }, + .{ "org.lr", {} }, + .{ "ls", {} }, + .{ "ac.ls", {} }, + .{ "biz.ls", {} }, + .{ "co.ls", {} }, + .{ "edu.ls", {} }, + .{ "gov.ls", {} }, + .{ "info.ls", {} }, + .{ "net.ls", {} }, + .{ "org.ls", {} }, + .{ "sc.ls", {} }, + .{ "lt", {} }, + .{ "gov.lt", {} }, + .{ "lu", {} }, + .{ "lv", {} }, + .{ "asn.lv", {} }, + .{ "com.lv", {} }, + .{ "conf.lv", {} }, + .{ "edu.lv", {} }, + .{ "gov.lv", {} }, + .{ "id.lv", {} }, + .{ "mil.lv", {} }, + .{ "net.lv", {} }, + .{ "org.lv", {} }, + .{ "ly", {} }, + .{ "com.ly", {} }, + .{ "edu.ly", {} }, + .{ "gov.ly", {} }, + .{ "id.ly", {} }, + .{ "med.ly", {} }, + .{ "net.ly", {} }, + .{ "org.ly", {} }, + .{ "plc.ly", {} }, + .{ "sch.ly", {} }, + .{ "ma", {} }, + .{ "ac.ma", {} }, + .{ "co.ma", {} }, + .{ "gov.ma", {} }, + .{ "net.ma", {} }, + .{ "org.ma", {} }, + .{ "press.ma", {} }, + .{ "mc", {} }, + .{ "asso.mc", {} }, + .{ "tm.mc", {} }, + .{ "md", {} }, + .{ "me", {} }, + .{ "ac.me", {} }, + .{ "co.me", {} }, + .{ "edu.me", {} }, + .{ "gov.me", {} }, + .{ "its.me", {} }, + .{ "net.me", {} }, + .{ "org.me", {} }, + .{ "priv.me", {} }, + .{ "mg", {} }, + .{ "co.mg", {} }, + .{ "com.mg", {} }, + .{ "edu.mg", {} }, + .{ "gov.mg", {} }, + .{ "mil.mg", {} }, + .{ "nom.mg", {} }, + .{ "org.mg", {} }, + .{ "prd.mg", {} }, + .{ "mh", {} }, + .{ "mil", {} }, + .{ "mk", {} }, + .{ "com.mk", {} }, + .{ "edu.mk", {} }, + .{ "gov.mk", {} }, + .{ "inf.mk", {} }, + .{ "name.mk", {} }, + .{ "net.mk", {} }, + .{ "org.mk", {} }, + .{ "ml", {} }, + .{ "ac.ml", {} }, + .{ "art.ml", {} }, + .{ "asso.ml", {} }, + .{ "com.ml", {} }, + .{ "edu.ml", {} }, + .{ "gouv.ml", {} }, + .{ "gov.ml", {} }, + .{ "info.ml", {} }, + .{ "inst.ml", {} }, + .{ "net.ml", {} }, + .{ "org.ml", {} }, + .{ "pr.ml", {} }, + .{ "presse.ml", {} }, + .{ "*.mm", {} }, + .{ "mn", {} }, + .{ "edu.mn", {} }, + .{ "gov.mn", {} }, + .{ "org.mn", {} }, + .{ "mo", {} }, + .{ "com.mo", {} }, + .{ "edu.mo", {} }, + .{ "gov.mo", {} }, + .{ "net.mo", {} }, + .{ "org.mo", {} }, + .{ "mobi", {} }, + .{ "mp", {} }, + .{ "mq", {} }, + .{ "mr", {} }, + .{ "gov.mr", {} }, + .{ "ms", {} }, + .{ "com.ms", {} }, + .{ "edu.ms", {} }, + .{ "gov.ms", {} }, + .{ "net.ms", {} }, + .{ "org.ms", {} }, + .{ "mt", {} }, + .{ "com.mt", {} }, + .{ "edu.mt", {} }, + .{ "net.mt", {} }, + .{ "org.mt", {} }, + .{ "mu", {} }, + .{ "ac.mu", {} }, + .{ "co.mu", {} }, + .{ "com.mu", {} }, + .{ "gov.mu", {} }, + .{ "net.mu", {} }, + .{ "or.mu", {} }, + .{ "org.mu", {} }, + .{ "museum", {} }, + .{ "mv", {} }, + .{ "aero.mv", {} }, + .{ "biz.mv", {} }, + .{ "com.mv", {} }, + .{ "coop.mv", {} }, + .{ "edu.mv", {} }, + .{ "gov.mv", {} }, + .{ "info.mv", {} }, + .{ "int.mv", {} }, + .{ "mil.mv", {} }, + .{ "museum.mv", {} }, + .{ "name.mv", {} }, + .{ "net.mv", {} }, + .{ "org.mv", {} }, + .{ "pro.mv", {} }, + .{ "mw", {} }, + .{ "ac.mw", {} }, + .{ "biz.mw", {} }, + .{ "co.mw", {} }, + .{ "com.mw", {} }, + .{ "coop.mw", {} }, + .{ "edu.mw", {} }, + .{ "gov.mw", {} }, + .{ "int.mw", {} }, + .{ "net.mw", {} }, + .{ "org.mw", {} }, + .{ "mx", {} }, + .{ "com.mx", {} }, + .{ "edu.mx", {} }, + .{ "gob.mx", {} }, + .{ "net.mx", {} }, + .{ "org.mx", {} }, + .{ "my", {} }, + .{ "biz.my", {} }, + .{ "com.my", {} }, + .{ "edu.my", {} }, + .{ "gov.my", {} }, + .{ "mil.my", {} }, + .{ "name.my", {} }, + .{ "net.my", {} }, + .{ "org.my", {} }, + .{ "mz", {} }, + .{ "ac.mz", {} }, + .{ "adv.mz", {} }, + .{ "co.mz", {} }, + .{ "edu.mz", {} }, + .{ "gov.mz", {} }, + .{ "mil.mz", {} }, + .{ "net.mz", {} }, + .{ "org.mz", {} }, + .{ "na", {} }, + .{ "alt.na", {} }, + .{ "co.na", {} }, + .{ "com.na", {} }, + .{ "gov.na", {} }, + .{ "net.na", {} }, + .{ "org.na", {} }, + .{ "name", {} }, + .{ "nc", {} }, + .{ "asso.nc", {} }, + .{ "nom.nc", {} }, + .{ "ne", {} }, + .{ "net", {} }, + .{ "nf", {} }, + .{ "arts.nf", {} }, + .{ "com.nf", {} }, + .{ "firm.nf", {} }, + .{ "info.nf", {} }, + .{ "net.nf", {} }, + .{ "other.nf", {} }, + .{ "per.nf", {} }, + .{ "rec.nf", {} }, + .{ "store.nf", {} }, + .{ "web.nf", {} }, + .{ "ng", {} }, + .{ "com.ng", {} }, + .{ "edu.ng", {} }, + .{ "gov.ng", {} }, + .{ "i.ng", {} }, + .{ "mil.ng", {} }, + .{ "mobi.ng", {} }, + .{ "name.ng", {} }, + .{ "net.ng", {} }, + .{ "org.ng", {} }, + .{ "sch.ng", {} }, + .{ "ni", {} }, + .{ "ac.ni", {} }, + .{ "biz.ni", {} }, + .{ "co.ni", {} }, + .{ "com.ni", {} }, + .{ "edu.ni", {} }, + .{ "gob.ni", {} }, + .{ "in.ni", {} }, + .{ "info.ni", {} }, + .{ "int.ni", {} }, + .{ "mil.ni", {} }, + .{ "net.ni", {} }, + .{ "nom.ni", {} }, + .{ "org.ni", {} }, + .{ "web.ni", {} }, + .{ "nl", {} }, + .{ "no", {} }, + .{ "fhs.no", {} }, + .{ "folkebibl.no", {} }, + .{ "fylkesbibl.no", {} }, + .{ "idrett.no", {} }, + .{ "museum.no", {} }, + .{ "priv.no", {} }, + .{ "vgs.no", {} }, + .{ "dep.no", {} }, + .{ "herad.no", {} }, + .{ "kommune.no", {} }, + .{ "mil.no", {} }, + .{ "stat.no", {} }, + .{ "aa.no", {} }, + .{ "ah.no", {} }, + .{ "bu.no", {} }, + .{ "fm.no", {} }, + .{ "hl.no", {} }, + .{ "hm.no", {} }, + .{ "jan-mayen.no", {} }, + .{ "mr.no", {} }, + .{ "nl.no", {} }, + .{ "nt.no", {} }, + .{ "of.no", {} }, + .{ "ol.no", {} }, + .{ "oslo.no", {} }, + .{ "rl.no", {} }, + .{ "sf.no", {} }, + .{ "st.no", {} }, + .{ "svalbard.no", {} }, + .{ "tm.no", {} }, + .{ "tr.no", {} }, + .{ "va.no", {} }, + .{ "vf.no", {} }, + .{ "gs.aa.no", {} }, + .{ "gs.ah.no", {} }, + .{ "gs.bu.no", {} }, + .{ "gs.fm.no", {} }, + .{ "gs.hl.no", {} }, + .{ "gs.hm.no", {} }, + .{ "gs.jan-mayen.no", {} }, + .{ "gs.mr.no", {} }, + .{ "gs.nl.no", {} }, + .{ "gs.nt.no", {} }, + .{ "gs.of.no", {} }, + .{ "gs.ol.no", {} }, + .{ "gs.oslo.no", {} }, + .{ "gs.rl.no", {} }, + .{ "gs.sf.no", {} }, + .{ "gs.st.no", {} }, + .{ "gs.svalbard.no", {} }, + .{ "gs.tm.no", {} }, + .{ "gs.tr.no", {} }, + .{ "gs.va.no", {} }, + .{ "gs.vf.no", {} }, + .{ "akrehamn.no", {} }, + .{ "åkrehamn.no", {} }, + .{ "algard.no", {} }, + .{ "ålgård.no", {} }, + .{ "arna.no", {} }, + .{ "bronnoysund.no", {} }, + .{ "brønnøysund.no", {} }, + .{ "brumunddal.no", {} }, + .{ "bryne.no", {} }, + .{ "drobak.no", {} }, + .{ "drøbak.no", {} }, + .{ "egersund.no", {} }, + .{ "fetsund.no", {} }, + .{ "floro.no", {} }, + .{ "florø.no", {} }, + .{ "fredrikstad.no", {} }, + .{ "hokksund.no", {} }, + .{ "honefoss.no", {} }, + .{ "hønefoss.no", {} }, + .{ "jessheim.no", {} }, + .{ "jorpeland.no", {} }, + .{ "jørpeland.no", {} }, + .{ "kirkenes.no", {} }, + .{ "kopervik.no", {} }, + .{ "krokstadelva.no", {} }, + .{ "langevag.no", {} }, + .{ "langevåg.no", {} }, + .{ "leirvik.no", {} }, + .{ "mjondalen.no", {} }, + .{ "mjøndalen.no", {} }, + .{ "mo-i-rana.no", {} }, + .{ "mosjoen.no", {} }, + .{ "mosjøen.no", {} }, + .{ "nesoddtangen.no", {} }, + .{ "orkanger.no", {} }, + .{ "osoyro.no", {} }, + .{ "osøyro.no", {} }, + .{ "raholt.no", {} }, + .{ "råholt.no", {} }, + .{ "sandnessjoen.no", {} }, + .{ "sandnessjøen.no", {} }, + .{ "skedsmokorset.no", {} }, + .{ "slattum.no", {} }, + .{ "spjelkavik.no", {} }, + .{ "stathelle.no", {} }, + .{ "stavern.no", {} }, + .{ "stjordalshalsen.no", {} }, + .{ "stjørdalshalsen.no", {} }, + .{ "tananger.no", {} }, + .{ "tranby.no", {} }, + .{ "vossevangen.no", {} }, + .{ "aarborte.no", {} }, + .{ "aejrie.no", {} }, + .{ "afjord.no", {} }, + .{ "åfjord.no", {} }, + .{ "agdenes.no", {} }, + .{ "nes.akershus.no", {} }, + .{ "aknoluokta.no", {} }, + .{ "ákŋoluokta.no", {} }, + .{ "al.no", {} }, + .{ "ål.no", {} }, + .{ "alaheadju.no", {} }, + .{ "álaheadju.no", {} }, + .{ "alesund.no", {} }, + .{ "ålesund.no", {} }, + .{ "alstahaug.no", {} }, + .{ "alta.no", {} }, + .{ "áltá.no", {} }, + .{ "alvdal.no", {} }, + .{ "amli.no", {} }, + .{ "åmli.no", {} }, + .{ "amot.no", {} }, + .{ "åmot.no", {} }, + .{ "andasuolo.no", {} }, + .{ "andebu.no", {} }, + .{ "andoy.no", {} }, + .{ "andøy.no", {} }, + .{ "ardal.no", {} }, + .{ "årdal.no", {} }, + .{ "aremark.no", {} }, + .{ "arendal.no", {} }, + .{ "ås.no", {} }, + .{ "aseral.no", {} }, + .{ "åseral.no", {} }, + .{ "asker.no", {} }, + .{ "askim.no", {} }, + .{ "askoy.no", {} }, + .{ "askøy.no", {} }, + .{ "askvoll.no", {} }, + .{ "asnes.no", {} }, + .{ "åsnes.no", {} }, + .{ "audnedaln.no", {} }, + .{ "aukra.no", {} }, + .{ "aure.no", {} }, + .{ "aurland.no", {} }, + .{ "aurskog-holand.no", {} }, + .{ "aurskog-høland.no", {} }, + .{ "austevoll.no", {} }, + .{ "austrheim.no", {} }, + .{ "averoy.no", {} }, + .{ "averøy.no", {} }, + .{ "badaddja.no", {} }, + .{ "bådåddjå.no", {} }, + .{ "bærum.no", {} }, + .{ "bahcavuotna.no", {} }, + .{ "báhcavuotna.no", {} }, + .{ "bahccavuotna.no", {} }, + .{ "báhccavuotna.no", {} }, + .{ "baidar.no", {} }, + .{ "báidár.no", {} }, + .{ "bajddar.no", {} }, + .{ "bájddar.no", {} }, + .{ "balat.no", {} }, + .{ "bálát.no", {} }, + .{ "balestrand.no", {} }, + .{ "ballangen.no", {} }, + .{ "balsfjord.no", {} }, + .{ "bamble.no", {} }, + .{ "bardu.no", {} }, + .{ "barum.no", {} }, + .{ "batsfjord.no", {} }, + .{ "båtsfjord.no", {} }, + .{ "bearalvahki.no", {} }, + .{ "bearalváhki.no", {} }, + .{ "beardu.no", {} }, + .{ "beiarn.no", {} }, + .{ "berg.no", {} }, + .{ "bergen.no", {} }, + .{ "berlevag.no", {} }, + .{ "berlevåg.no", {} }, + .{ "bievat.no", {} }, + .{ "bievát.no", {} }, + .{ "bindal.no", {} }, + .{ "birkenes.no", {} }, + .{ "bjarkoy.no", {} }, + .{ "bjarkøy.no", {} }, + .{ "bjerkreim.no", {} }, + .{ "bjugn.no", {} }, + .{ "bodo.no", {} }, + .{ "bodø.no", {} }, + .{ "bokn.no", {} }, + .{ "bomlo.no", {} }, + .{ "bømlo.no", {} }, + .{ "bremanger.no", {} }, + .{ "bronnoy.no", {} }, + .{ "brønnøy.no", {} }, + .{ "budejju.no", {} }, + .{ "nes.buskerud.no", {} }, + .{ "bygland.no", {} }, + .{ "bykle.no", {} }, + .{ "cahcesuolo.no", {} }, + .{ "čáhcesuolo.no", {} }, + .{ "davvenjarga.no", {} }, + .{ "davvenjárga.no", {} }, + .{ "davvesiida.no", {} }, + .{ "deatnu.no", {} }, + .{ "dielddanuorri.no", {} }, + .{ "divtasvuodna.no", {} }, + .{ "divttasvuotna.no", {} }, + .{ "donna.no", {} }, + .{ "dønna.no", {} }, + .{ "dovre.no", {} }, + .{ "drammen.no", {} }, + .{ "drangedal.no", {} }, + .{ "dyroy.no", {} }, + .{ "dyrøy.no", {} }, + .{ "eid.no", {} }, + .{ "eidfjord.no", {} }, + .{ "eidsberg.no", {} }, + .{ "eidskog.no", {} }, + .{ "eidsvoll.no", {} }, + .{ "eigersund.no", {} }, + .{ "elverum.no", {} }, + .{ "enebakk.no", {} }, + .{ "engerdal.no", {} }, + .{ "etne.no", {} }, + .{ "etnedal.no", {} }, + .{ "evenassi.no", {} }, + .{ "evenášši.no", {} }, + .{ "evenes.no", {} }, + .{ "evje-og-hornnes.no", {} }, + .{ "farsund.no", {} }, + .{ "fauske.no", {} }, + .{ "fedje.no", {} }, + .{ "fet.no", {} }, + .{ "finnoy.no", {} }, + .{ "finnøy.no", {} }, + .{ "fitjar.no", {} }, + .{ "fjaler.no", {} }, + .{ "fjell.no", {} }, + .{ "fla.no", {} }, + .{ "flå.no", {} }, + .{ "flakstad.no", {} }, + .{ "flatanger.no", {} }, + .{ "flekkefjord.no", {} }, + .{ "flesberg.no", {} }, + .{ "flora.no", {} }, + .{ "folldal.no", {} }, + .{ "forde.no", {} }, + .{ "førde.no", {} }, + .{ "forsand.no", {} }, + .{ "fosnes.no", {} }, + .{ "fræna.no", {} }, + .{ "frana.no", {} }, + .{ "frei.no", {} }, + .{ "frogn.no", {} }, + .{ "froland.no", {} }, + .{ "frosta.no", {} }, + .{ "froya.no", {} }, + .{ "frøya.no", {} }, + .{ "fuoisku.no", {} }, + .{ "fuossko.no", {} }, + .{ "fusa.no", {} }, + .{ "fyresdal.no", {} }, + .{ "gaivuotna.no", {} }, + .{ "gáivuotna.no", {} }, + .{ "galsa.no", {} }, + .{ "gálsá.no", {} }, + .{ "gamvik.no", {} }, + .{ "gangaviika.no", {} }, + .{ "gáŋgaviika.no", {} }, + .{ "gaular.no", {} }, + .{ "gausdal.no", {} }, + .{ "giehtavuoatna.no", {} }, + .{ "gildeskal.no", {} }, + .{ "gildeskål.no", {} }, + .{ "giske.no", {} }, + .{ "gjemnes.no", {} }, + .{ "gjerdrum.no", {} }, + .{ "gjerstad.no", {} }, + .{ "gjesdal.no", {} }, + .{ "gjovik.no", {} }, + .{ "gjøvik.no", {} }, + .{ "gloppen.no", {} }, + .{ "gol.no", {} }, + .{ "gran.no", {} }, + .{ "grane.no", {} }, + .{ "granvin.no", {} }, + .{ "gratangen.no", {} }, + .{ "grimstad.no", {} }, + .{ "grong.no", {} }, + .{ "grue.no", {} }, + .{ "gulen.no", {} }, + .{ "guovdageaidnu.no", {} }, + .{ "ha.no", {} }, + .{ "hå.no", {} }, + .{ "habmer.no", {} }, + .{ "hábmer.no", {} }, + .{ "hadsel.no", {} }, + .{ "hægebostad.no", {} }, + .{ "hagebostad.no", {} }, + .{ "halden.no", {} }, + .{ "halsa.no", {} }, + .{ "hamar.no", {} }, + .{ "hamaroy.no", {} }, + .{ "hammarfeasta.no", {} }, + .{ "hámmárfeasta.no", {} }, + .{ "hammerfest.no", {} }, + .{ "hapmir.no", {} }, + .{ "hápmir.no", {} }, + .{ "haram.no", {} }, + .{ "hareid.no", {} }, + .{ "harstad.no", {} }, + .{ "hasvik.no", {} }, + .{ "hattfjelldal.no", {} }, + .{ "haugesund.no", {} }, + .{ "os.hedmark.no", {} }, + .{ "valer.hedmark.no", {} }, + .{ "våler.hedmark.no", {} }, + .{ "hemne.no", {} }, + .{ "hemnes.no", {} }, + .{ "hemsedal.no", {} }, + .{ "hitra.no", {} }, + .{ "hjartdal.no", {} }, + .{ "hjelmeland.no", {} }, + .{ "hobol.no", {} }, + .{ "hobøl.no", {} }, + .{ "hof.no", {} }, + .{ "hol.no", {} }, + .{ "hole.no", {} }, + .{ "holmestrand.no", {} }, + .{ "holtalen.no", {} }, + .{ "holtålen.no", {} }, + .{ "os.hordaland.no", {} }, + .{ "hornindal.no", {} }, + .{ "horten.no", {} }, + .{ "hoyanger.no", {} }, + .{ "høyanger.no", {} }, + .{ "hoylandet.no", {} }, + .{ "høylandet.no", {} }, + .{ "hurdal.no", {} }, + .{ "hurum.no", {} }, + .{ "hvaler.no", {} }, + .{ "hyllestad.no", {} }, + .{ "ibestad.no", {} }, + .{ "inderoy.no", {} }, + .{ "inderøy.no", {} }, + .{ "iveland.no", {} }, + .{ "ivgu.no", {} }, + .{ "jevnaker.no", {} }, + .{ "jolster.no", {} }, + .{ "jølster.no", {} }, + .{ "jondal.no", {} }, + .{ "kafjord.no", {} }, + .{ "kåfjord.no", {} }, + .{ "karasjohka.no", {} }, + .{ "kárášjohka.no", {} }, + .{ "karasjok.no", {} }, + .{ "karlsoy.no", {} }, + .{ "karmoy.no", {} }, + .{ "karmøy.no", {} }, + .{ "kautokeino.no", {} }, + .{ "klabu.no", {} }, + .{ "klæbu.no", {} }, + .{ "klepp.no", {} }, + .{ "kongsberg.no", {} }, + .{ "kongsvinger.no", {} }, + .{ "kraanghke.no", {} }, + .{ "kråanghke.no", {} }, + .{ "kragero.no", {} }, + .{ "kragerø.no", {} }, + .{ "kristiansand.no", {} }, + .{ "kristiansund.no", {} }, + .{ "krodsherad.no", {} }, + .{ "krødsherad.no", {} }, + .{ "kvæfjord.no", {} }, + .{ "kvænangen.no", {} }, + .{ "kvafjord.no", {} }, + .{ "kvalsund.no", {} }, + .{ "kvam.no", {} }, + .{ "kvanangen.no", {} }, + .{ "kvinesdal.no", {} }, + .{ "kvinnherad.no", {} }, + .{ "kviteseid.no", {} }, + .{ "kvitsoy.no", {} }, + .{ "kvitsøy.no", {} }, + .{ "laakesvuemie.no", {} }, + .{ "lærdal.no", {} }, + .{ "lahppi.no", {} }, + .{ "láhppi.no", {} }, + .{ "lardal.no", {} }, + .{ "larvik.no", {} }, + .{ "lavagis.no", {} }, + .{ "lavangen.no", {} }, + .{ "leangaviika.no", {} }, + .{ "leaŋgaviika.no", {} }, + .{ "lebesby.no", {} }, + .{ "leikanger.no", {} }, + .{ "leirfjord.no", {} }, + .{ "leka.no", {} }, + .{ "leksvik.no", {} }, + .{ "lenvik.no", {} }, + .{ "lerdal.no", {} }, + .{ "lesja.no", {} }, + .{ "levanger.no", {} }, + .{ "lier.no", {} }, + .{ "lierne.no", {} }, + .{ "lillehammer.no", {} }, + .{ "lillesand.no", {} }, + .{ "lindas.no", {} }, + .{ "lindås.no", {} }, + .{ "lindesnes.no", {} }, + .{ "loabat.no", {} }, + .{ "loabát.no", {} }, + .{ "lodingen.no", {} }, + .{ "lødingen.no", {} }, + .{ "lom.no", {} }, + .{ "loppa.no", {} }, + .{ "lorenskog.no", {} }, + .{ "lørenskog.no", {} }, + .{ "loten.no", {} }, + .{ "løten.no", {} }, + .{ "lund.no", {} }, + .{ "lunner.no", {} }, + .{ "luroy.no", {} }, + .{ "lurøy.no", {} }, + .{ "luster.no", {} }, + .{ "lyngdal.no", {} }, + .{ "lyngen.no", {} }, + .{ "malatvuopmi.no", {} }, + .{ "málatvuopmi.no", {} }, + .{ "malselv.no", {} }, + .{ "målselv.no", {} }, + .{ "malvik.no", {} }, + .{ "mandal.no", {} }, + .{ "marker.no", {} }, + .{ "marnardal.no", {} }, + .{ "masfjorden.no", {} }, + .{ "masoy.no", {} }, + .{ "måsøy.no", {} }, + .{ "matta-varjjat.no", {} }, + .{ "mátta-várjjat.no", {} }, + .{ "meland.no", {} }, + .{ "meldal.no", {} }, + .{ "melhus.no", {} }, + .{ "meloy.no", {} }, + .{ "meløy.no", {} }, + .{ "meraker.no", {} }, + .{ "meråker.no", {} }, + .{ "midsund.no", {} }, + .{ "midtre-gauldal.no", {} }, + .{ "moareke.no", {} }, + .{ "moåreke.no", {} }, + .{ "modalen.no", {} }, + .{ "modum.no", {} }, + .{ "molde.no", {} }, + .{ "heroy.more-og-romsdal.no", {} }, + .{ "sande.more-og-romsdal.no", {} }, + .{ "herøy.møre-og-romsdal.no", {} }, + .{ "sande.møre-og-romsdal.no", {} }, + .{ "moskenes.no", {} }, + .{ "moss.no", {} }, + .{ "mosvik.no", {} }, + .{ "muosat.no", {} }, + .{ "muosát.no", {} }, + .{ "naamesjevuemie.no", {} }, + .{ "nååmesjevuemie.no", {} }, + .{ "nærøy.no", {} }, + .{ "namdalseid.no", {} }, + .{ "namsos.no", {} }, + .{ "namsskogan.no", {} }, + .{ "nannestad.no", {} }, + .{ "naroy.no", {} }, + .{ "narviika.no", {} }, + .{ "narvik.no", {} }, + .{ "naustdal.no", {} }, + .{ "navuotna.no", {} }, + .{ "návuotna.no", {} }, + .{ "nedre-eiker.no", {} }, + .{ "nesna.no", {} }, + .{ "nesodden.no", {} }, + .{ "nesseby.no", {} }, + .{ "nesset.no", {} }, + .{ "nissedal.no", {} }, + .{ "nittedal.no", {} }, + .{ "nord-aurdal.no", {} }, + .{ "nord-fron.no", {} }, + .{ "nord-odal.no", {} }, + .{ "norddal.no", {} }, + .{ "nordkapp.no", {} }, + .{ "bo.nordland.no", {} }, + .{ "bø.nordland.no", {} }, + .{ "heroy.nordland.no", {} }, + .{ "herøy.nordland.no", {} }, + .{ "nordre-land.no", {} }, + .{ "nordreisa.no", {} }, + .{ "nore-og-uvdal.no", {} }, + .{ "notodden.no", {} }, + .{ "notteroy.no", {} }, + .{ "nøtterøy.no", {} }, + .{ "odda.no", {} }, + .{ "oksnes.no", {} }, + .{ "øksnes.no", {} }, + .{ "omasvuotna.no", {} }, + .{ "oppdal.no", {} }, + .{ "oppegard.no", {} }, + .{ "oppegård.no", {} }, + .{ "orkdal.no", {} }, + .{ "orland.no", {} }, + .{ "ørland.no", {} }, + .{ "orskog.no", {} }, + .{ "ørskog.no", {} }, + .{ "orsta.no", {} }, + .{ "ørsta.no", {} }, + .{ "osen.no", {} }, + .{ "osteroy.no", {} }, + .{ "osterøy.no", {} }, + .{ "valer.ostfold.no", {} }, + .{ "våler.østfold.no", {} }, + .{ "ostre-toten.no", {} }, + .{ "østre-toten.no", {} }, + .{ "overhalla.no", {} }, + .{ "ovre-eiker.no", {} }, + .{ "øvre-eiker.no", {} }, + .{ "oyer.no", {} }, + .{ "øyer.no", {} }, + .{ "oygarden.no", {} }, + .{ "øygarden.no", {} }, + .{ "oystre-slidre.no", {} }, + .{ "øystre-slidre.no", {} }, + .{ "porsanger.no", {} }, + .{ "porsangu.no", {} }, + .{ "porsáŋgu.no", {} }, + .{ "porsgrunn.no", {} }, + .{ "rade.no", {} }, + .{ "råde.no", {} }, + .{ "radoy.no", {} }, + .{ "radøy.no", {} }, + .{ "rælingen.no", {} }, + .{ "rahkkeravju.no", {} }, + .{ "ráhkkerávju.no", {} }, + .{ "raisa.no", {} }, + .{ "ráisa.no", {} }, + .{ "rakkestad.no", {} }, + .{ "ralingen.no", {} }, + .{ "rana.no", {} }, + .{ "randaberg.no", {} }, + .{ "rauma.no", {} }, + .{ "rendalen.no", {} }, + .{ "rennebu.no", {} }, + .{ "rennesoy.no", {} }, + .{ "rennesøy.no", {} }, + .{ "rindal.no", {} }, + .{ "ringebu.no", {} }, + .{ "ringerike.no", {} }, + .{ "ringsaker.no", {} }, + .{ "risor.no", {} }, + .{ "risør.no", {} }, + .{ "rissa.no", {} }, + .{ "roan.no", {} }, + .{ "rodoy.no", {} }, + .{ "rødøy.no", {} }, + .{ "rollag.no", {} }, + .{ "romsa.no", {} }, + .{ "romskog.no", {} }, + .{ "rømskog.no", {} }, + .{ "roros.no", {} }, + .{ "røros.no", {} }, + .{ "rost.no", {} }, + .{ "røst.no", {} }, + .{ "royken.no", {} }, + .{ "røyken.no", {} }, + .{ "royrvik.no", {} }, + .{ "røyrvik.no", {} }, + .{ "ruovat.no", {} }, + .{ "rygge.no", {} }, + .{ "salangen.no", {} }, + .{ "salat.no", {} }, + .{ "sálat.no", {} }, + .{ "sálát.no", {} }, + .{ "saltdal.no", {} }, + .{ "samnanger.no", {} }, + .{ "sandefjord.no", {} }, + .{ "sandnes.no", {} }, + .{ "sandoy.no", {} }, + .{ "sandøy.no", {} }, + .{ "sarpsborg.no", {} }, + .{ "sauda.no", {} }, + .{ "sauherad.no", {} }, + .{ "sel.no", {} }, + .{ "selbu.no", {} }, + .{ "selje.no", {} }, + .{ "seljord.no", {} }, + .{ "siellak.no", {} }, + .{ "sigdal.no", {} }, + .{ "siljan.no", {} }, + .{ "sirdal.no", {} }, + .{ "skanit.no", {} }, + .{ "skánit.no", {} }, + .{ "skanland.no", {} }, + .{ "skånland.no", {} }, + .{ "skaun.no", {} }, + .{ "skedsmo.no", {} }, + .{ "ski.no", {} }, + .{ "skien.no", {} }, + .{ "skierva.no", {} }, + .{ "skiervá.no", {} }, + .{ "skiptvet.no", {} }, + .{ "skjak.no", {} }, + .{ "skjåk.no", {} }, + .{ "skjervoy.no", {} }, + .{ "skjervøy.no", {} }, + .{ "skodje.no", {} }, + .{ "smola.no", {} }, + .{ "smøla.no", {} }, + .{ "snaase.no", {} }, + .{ "snåase.no", {} }, + .{ "snasa.no", {} }, + .{ "snåsa.no", {} }, + .{ "snillfjord.no", {} }, + .{ "snoasa.no", {} }, + .{ "sogndal.no", {} }, + .{ "sogne.no", {} }, + .{ "søgne.no", {} }, + .{ "sokndal.no", {} }, + .{ "sola.no", {} }, + .{ "solund.no", {} }, + .{ "somna.no", {} }, + .{ "sømna.no", {} }, + .{ "sondre-land.no", {} }, + .{ "søndre-land.no", {} }, + .{ "songdalen.no", {} }, + .{ "sor-aurdal.no", {} }, + .{ "sør-aurdal.no", {} }, + .{ "sor-fron.no", {} }, + .{ "sør-fron.no", {} }, + .{ "sor-odal.no", {} }, + .{ "sør-odal.no", {} }, + .{ "sor-varanger.no", {} }, + .{ "sør-varanger.no", {} }, + .{ "sorfold.no", {} }, + .{ "sørfold.no", {} }, + .{ "sorreisa.no", {} }, + .{ "sørreisa.no", {} }, + .{ "sortland.no", {} }, + .{ "sorum.no", {} }, + .{ "sørum.no", {} }, + .{ "spydeberg.no", {} }, + .{ "stange.no", {} }, + .{ "stavanger.no", {} }, + .{ "steigen.no", {} }, + .{ "steinkjer.no", {} }, + .{ "stjordal.no", {} }, + .{ "stjørdal.no", {} }, + .{ "stokke.no", {} }, + .{ "stor-elvdal.no", {} }, + .{ "stord.no", {} }, + .{ "stordal.no", {} }, + .{ "storfjord.no", {} }, + .{ "strand.no", {} }, + .{ "stranda.no", {} }, + .{ "stryn.no", {} }, + .{ "sula.no", {} }, + .{ "suldal.no", {} }, + .{ "sund.no", {} }, + .{ "sunndal.no", {} }, + .{ "surnadal.no", {} }, + .{ "sveio.no", {} }, + .{ "svelvik.no", {} }, + .{ "sykkylven.no", {} }, + .{ "tana.no", {} }, + .{ "bo.telemark.no", {} }, + .{ "bø.telemark.no", {} }, + .{ "time.no", {} }, + .{ "tingvoll.no", {} }, + .{ "tinn.no", {} }, + .{ "tjeldsund.no", {} }, + .{ "tjome.no", {} }, + .{ "tjøme.no", {} }, + .{ "tokke.no", {} }, + .{ "tolga.no", {} }, + .{ "tonsberg.no", {} }, + .{ "tønsberg.no", {} }, + .{ "torsken.no", {} }, + .{ "træna.no", {} }, + .{ "trana.no", {} }, + .{ "tranoy.no", {} }, + .{ "tranøy.no", {} }, + .{ "troandin.no", {} }, + .{ "trogstad.no", {} }, + .{ "trøgstad.no", {} }, + .{ "tromsa.no", {} }, + .{ "tromso.no", {} }, + .{ "tromsø.no", {} }, + .{ "trondheim.no", {} }, + .{ "trysil.no", {} }, + .{ "tvedestrand.no", {} }, + .{ "tydal.no", {} }, + .{ "tynset.no", {} }, + .{ "tysfjord.no", {} }, + .{ "tysnes.no", {} }, + .{ "tysvær.no", {} }, + .{ "tysvar.no", {} }, + .{ "ullensaker.no", {} }, + .{ "ullensvang.no", {} }, + .{ "ulvik.no", {} }, + .{ "unjarga.no", {} }, + .{ "unjárga.no", {} }, + .{ "utsira.no", {} }, + .{ "vaapste.no", {} }, + .{ "vadso.no", {} }, + .{ "vadsø.no", {} }, + .{ "værøy.no", {} }, + .{ "vaga.no", {} }, + .{ "vågå.no", {} }, + .{ "vagan.no", {} }, + .{ "vågan.no", {} }, + .{ "vagsoy.no", {} }, + .{ "vågsøy.no", {} }, + .{ "vaksdal.no", {} }, + .{ "valle.no", {} }, + .{ "vang.no", {} }, + .{ "vanylven.no", {} }, + .{ "vardo.no", {} }, + .{ "vardø.no", {} }, + .{ "varggat.no", {} }, + .{ "várggát.no", {} }, + .{ "varoy.no", {} }, + .{ "vefsn.no", {} }, + .{ "vega.no", {} }, + .{ "vegarshei.no", {} }, + .{ "vegårshei.no", {} }, + .{ "vennesla.no", {} }, + .{ "verdal.no", {} }, + .{ "verran.no", {} }, + .{ "vestby.no", {} }, + .{ "sande.vestfold.no", {} }, + .{ "vestnes.no", {} }, + .{ "vestre-slidre.no", {} }, + .{ "vestre-toten.no", {} }, + .{ "vestvagoy.no", {} }, + .{ "vestvågøy.no", {} }, + .{ "vevelstad.no", {} }, + .{ "vik.no", {} }, + .{ "vikna.no", {} }, + .{ "vindafjord.no", {} }, + .{ "voagat.no", {} }, + .{ "volda.no", {} }, + .{ "voss.no", {} }, + .{ "*.np", {} }, + .{ "nr", {} }, + .{ "biz.nr", {} }, + .{ "com.nr", {} }, + .{ "edu.nr", {} }, + .{ "gov.nr", {} }, + .{ "info.nr", {} }, + .{ "net.nr", {} }, + .{ "org.nr", {} }, + .{ "nu", {} }, + .{ "nz", {} }, + .{ "ac.nz", {} }, + .{ "co.nz", {} }, + .{ "cri.nz", {} }, + .{ "geek.nz", {} }, + .{ "gen.nz", {} }, + .{ "govt.nz", {} }, + .{ "health.nz", {} }, + .{ "iwi.nz", {} }, + .{ "kiwi.nz", {} }, + .{ "maori.nz", {} }, + .{ "māori.nz", {} }, + .{ "mil.nz", {} }, + .{ "net.nz", {} }, + .{ "org.nz", {} }, + .{ "parliament.nz", {} }, + .{ "school.nz", {} }, + .{ "om", {} }, + .{ "co.om", {} }, + .{ "com.om", {} }, + .{ "edu.om", {} }, + .{ "gov.om", {} }, + .{ "med.om", {} }, + .{ "museum.om", {} }, + .{ "net.om", {} }, + .{ "org.om", {} }, + .{ "pro.om", {} }, + .{ "onion", {} }, + .{ "org", {} }, + .{ "pa", {} }, + .{ "abo.pa", {} }, + .{ "ac.pa", {} }, + .{ "com.pa", {} }, + .{ "edu.pa", {} }, + .{ "gob.pa", {} }, + .{ "ing.pa", {} }, + .{ "med.pa", {} }, + .{ "net.pa", {} }, + .{ "nom.pa", {} }, + .{ "org.pa", {} }, + .{ "sld.pa", {} }, + .{ "pe", {} }, + .{ "com.pe", {} }, + .{ "edu.pe", {} }, + .{ "gob.pe", {} }, + .{ "mil.pe", {} }, + .{ "net.pe", {} }, + .{ "nom.pe", {} }, + .{ "org.pe", {} }, + .{ "pf", {} }, + .{ "com.pf", {} }, + .{ "edu.pf", {} }, + .{ "org.pf", {} }, + .{ "*.pg", {} }, + .{ "ph", {} }, + .{ "com.ph", {} }, + .{ "edu.ph", {} }, + .{ "gov.ph", {} }, + .{ "i.ph", {} }, + .{ "mil.ph", {} }, + .{ "net.ph", {} }, + .{ "ngo.ph", {} }, + .{ "org.ph", {} }, + .{ "pk", {} }, + .{ "ac.pk", {} }, + .{ "biz.pk", {} }, + .{ "com.pk", {} }, + .{ "edu.pk", {} }, + .{ "fam.pk", {} }, + .{ "gkp.pk", {} }, + .{ "gob.pk", {} }, + .{ "gog.pk", {} }, + .{ "gok.pk", {} }, + .{ "gop.pk", {} }, + .{ "gos.pk", {} }, + .{ "gov.pk", {} }, + .{ "net.pk", {} }, + .{ "org.pk", {} }, + .{ "web.pk", {} }, + .{ "pl", {} }, + .{ "com.pl", {} }, + .{ "net.pl", {} }, + .{ "org.pl", {} }, + .{ "agro.pl", {} }, + .{ "aid.pl", {} }, + .{ "atm.pl", {} }, + .{ "auto.pl", {} }, + .{ "biz.pl", {} }, + .{ "edu.pl", {} }, + .{ "gmina.pl", {} }, + .{ "gsm.pl", {} }, + .{ "info.pl", {} }, + .{ "mail.pl", {} }, + .{ "media.pl", {} }, + .{ "miasta.pl", {} }, + .{ "mil.pl", {} }, + .{ "nieruchomosci.pl", {} }, + .{ "nom.pl", {} }, + .{ "pc.pl", {} }, + .{ "powiat.pl", {} }, + .{ "priv.pl", {} }, + .{ "realestate.pl", {} }, + .{ "rel.pl", {} }, + .{ "sex.pl", {} }, + .{ "shop.pl", {} }, + .{ "sklep.pl", {} }, + .{ "sos.pl", {} }, + .{ "szkola.pl", {} }, + .{ "targi.pl", {} }, + .{ "tm.pl", {} }, + .{ "tourism.pl", {} }, + .{ "travel.pl", {} }, + .{ "turystyka.pl", {} }, + .{ "gov.pl", {} }, + .{ "ap.gov.pl", {} }, + .{ "griw.gov.pl", {} }, + .{ "ic.gov.pl", {} }, + .{ "is.gov.pl", {} }, + .{ "kmpsp.gov.pl", {} }, + .{ "konsulat.gov.pl", {} }, + .{ "kppsp.gov.pl", {} }, + .{ "kwp.gov.pl", {} }, + .{ "kwpsp.gov.pl", {} }, + .{ "mup.gov.pl", {} }, + .{ "mw.gov.pl", {} }, + .{ "oia.gov.pl", {} }, + .{ "oirm.gov.pl", {} }, + .{ "oke.gov.pl", {} }, + .{ "oow.gov.pl", {} }, + .{ "oschr.gov.pl", {} }, + .{ "oum.gov.pl", {} }, + .{ "pa.gov.pl", {} }, + .{ "pinb.gov.pl", {} }, + .{ "piw.gov.pl", {} }, + .{ "po.gov.pl", {} }, + .{ "pr.gov.pl", {} }, + .{ "psp.gov.pl", {} }, + .{ "psse.gov.pl", {} }, + .{ "pup.gov.pl", {} }, + .{ "rzgw.gov.pl", {} }, + .{ "sa.gov.pl", {} }, + .{ "sdn.gov.pl", {} }, + .{ "sko.gov.pl", {} }, + .{ "so.gov.pl", {} }, + .{ "sr.gov.pl", {} }, + .{ "starostwo.gov.pl", {} }, + .{ "ug.gov.pl", {} }, + .{ "ugim.gov.pl", {} }, + .{ "um.gov.pl", {} }, + .{ "umig.gov.pl", {} }, + .{ "upow.gov.pl", {} }, + .{ "uppo.gov.pl", {} }, + .{ "us.gov.pl", {} }, + .{ "uw.gov.pl", {} }, + .{ "uzs.gov.pl", {} }, + .{ "wif.gov.pl", {} }, + .{ "wiih.gov.pl", {} }, + .{ "winb.gov.pl", {} }, + .{ "wios.gov.pl", {} }, + .{ "witd.gov.pl", {} }, + .{ "wiw.gov.pl", {} }, + .{ "wkz.gov.pl", {} }, + .{ "wsa.gov.pl", {} }, + .{ "wskr.gov.pl", {} }, + .{ "wsse.gov.pl", {} }, + .{ "wuoz.gov.pl", {} }, + .{ "wzmiuw.gov.pl", {} }, + .{ "zp.gov.pl", {} }, + .{ "zpisdn.gov.pl", {} }, + .{ "augustow.pl", {} }, + .{ "babia-gora.pl", {} }, + .{ "bedzin.pl", {} }, + .{ "beskidy.pl", {} }, + .{ "bialowieza.pl", {} }, + .{ "bialystok.pl", {} }, + .{ "bielawa.pl", {} }, + .{ "bieszczady.pl", {} }, + .{ "boleslawiec.pl", {} }, + .{ "bydgoszcz.pl", {} }, + .{ "bytom.pl", {} }, + .{ "cieszyn.pl", {} }, + .{ "czeladz.pl", {} }, + .{ "czest.pl", {} }, + .{ "dlugoleka.pl", {} }, + .{ "elblag.pl", {} }, + .{ "elk.pl", {} }, + .{ "glogow.pl", {} }, + .{ "gniezno.pl", {} }, + .{ "gorlice.pl", {} }, + .{ "grajewo.pl", {} }, + .{ "ilawa.pl", {} }, + .{ "jaworzno.pl", {} }, + .{ "jelenia-gora.pl", {} }, + .{ "jgora.pl", {} }, + .{ "kalisz.pl", {} }, + .{ "karpacz.pl", {} }, + .{ "kartuzy.pl", {} }, + .{ "kaszuby.pl", {} }, + .{ "katowice.pl", {} }, + .{ "kazimierz-dolny.pl", {} }, + .{ "kepno.pl", {} }, + .{ "ketrzyn.pl", {} }, + .{ "klodzko.pl", {} }, + .{ "kobierzyce.pl", {} }, + .{ "kolobrzeg.pl", {} }, + .{ "konin.pl", {} }, + .{ "konskowola.pl", {} }, + .{ "kutno.pl", {} }, + .{ "lapy.pl", {} }, + .{ "lebork.pl", {} }, + .{ "legnica.pl", {} }, + .{ "lezajsk.pl", {} }, + .{ "limanowa.pl", {} }, + .{ "lomza.pl", {} }, + .{ "lowicz.pl", {} }, + .{ "lubin.pl", {} }, + .{ "lukow.pl", {} }, + .{ "malbork.pl", {} }, + .{ "malopolska.pl", {} }, + .{ "mazowsze.pl", {} }, + .{ "mazury.pl", {} }, + .{ "mielec.pl", {} }, + .{ "mielno.pl", {} }, + .{ "mragowo.pl", {} }, + .{ "naklo.pl", {} }, + .{ "nowaruda.pl", {} }, + .{ "nysa.pl", {} }, + .{ "olawa.pl", {} }, + .{ "olecko.pl", {} }, + .{ "olkusz.pl", {} }, + .{ "olsztyn.pl", {} }, + .{ "opoczno.pl", {} }, + .{ "opole.pl", {} }, + .{ "ostroda.pl", {} }, + .{ "ostroleka.pl", {} }, + .{ "ostrowiec.pl", {} }, + .{ "ostrowwlkp.pl", {} }, + .{ "pila.pl", {} }, + .{ "pisz.pl", {} }, + .{ "podhale.pl", {} }, + .{ "podlasie.pl", {} }, + .{ "polkowice.pl", {} }, + .{ "pomorskie.pl", {} }, + .{ "pomorze.pl", {} }, + .{ "prochowice.pl", {} }, + .{ "pruszkow.pl", {} }, + .{ "przeworsk.pl", {} }, + .{ "pulawy.pl", {} }, + .{ "radom.pl", {} }, + .{ "rawa-maz.pl", {} }, + .{ "rybnik.pl", {} }, + .{ "rzeszow.pl", {} }, + .{ "sanok.pl", {} }, + .{ "sejny.pl", {} }, + .{ "skoczow.pl", {} }, + .{ "slask.pl", {} }, + .{ "slupsk.pl", {} }, + .{ "sosnowiec.pl", {} }, + .{ "stalowa-wola.pl", {} }, + .{ "starachowice.pl", {} }, + .{ "stargard.pl", {} }, + .{ "suwalki.pl", {} }, + .{ "swidnica.pl", {} }, + .{ "swiebodzin.pl", {} }, + .{ "swinoujscie.pl", {} }, + .{ "szczecin.pl", {} }, + .{ "szczytno.pl", {} }, + .{ "tarnobrzeg.pl", {} }, + .{ "tgory.pl", {} }, + .{ "turek.pl", {} }, + .{ "tychy.pl", {} }, + .{ "ustka.pl", {} }, + .{ "walbrzych.pl", {} }, + .{ "warmia.pl", {} }, + .{ "warszawa.pl", {} }, + .{ "waw.pl", {} }, + .{ "wegrow.pl", {} }, + .{ "wielun.pl", {} }, + .{ "wlocl.pl", {} }, + .{ "wloclawek.pl", {} }, + .{ "wodzislaw.pl", {} }, + .{ "wolomin.pl", {} }, + .{ "wroclaw.pl", {} }, + .{ "zachpomor.pl", {} }, + .{ "zagan.pl", {} }, + .{ "zarow.pl", {} }, + .{ "zgora.pl", {} }, + .{ "zgorzelec.pl", {} }, + .{ "pm", {} }, + .{ "pn", {} }, + .{ "co.pn", {} }, + .{ "edu.pn", {} }, + .{ "gov.pn", {} }, + .{ "net.pn", {} }, + .{ "org.pn", {} }, + .{ "post", {} }, + .{ "pr", {} }, + .{ "biz.pr", {} }, + .{ "com.pr", {} }, + .{ "edu.pr", {} }, + .{ "gov.pr", {} }, + .{ "info.pr", {} }, + .{ "isla.pr", {} }, + .{ "name.pr", {} }, + .{ "net.pr", {} }, + .{ "org.pr", {} }, + .{ "pro.pr", {} }, + .{ "ac.pr", {} }, + .{ "est.pr", {} }, + .{ "prof.pr", {} }, + .{ "pro", {} }, + .{ "aaa.pro", {} }, + .{ "aca.pro", {} }, + .{ "acct.pro", {} }, + .{ "avocat.pro", {} }, + .{ "bar.pro", {} }, + .{ "cpa.pro", {} }, + .{ "eng.pro", {} }, + .{ "jur.pro", {} }, + .{ "law.pro", {} }, + .{ "med.pro", {} }, + .{ "recht.pro", {} }, + .{ "ps", {} }, + .{ "com.ps", {} }, + .{ "edu.ps", {} }, + .{ "gov.ps", {} }, + .{ "net.ps", {} }, + .{ "org.ps", {} }, + .{ "plo.ps", {} }, + .{ "sec.ps", {} }, + .{ "pt", {} }, + .{ "com.pt", {} }, + .{ "edu.pt", {} }, + .{ "gov.pt", {} }, + .{ "int.pt", {} }, + .{ "net.pt", {} }, + .{ "nome.pt", {} }, + .{ "org.pt", {} }, + .{ "publ.pt", {} }, + .{ "pw", {} }, + .{ "gov.pw", {} }, + .{ "py", {} }, + .{ "com.py", {} }, + .{ "coop.py", {} }, + .{ "edu.py", {} }, + .{ "gov.py", {} }, + .{ "mil.py", {} }, + .{ "net.py", {} }, + .{ "org.py", {} }, + .{ "qa", {} }, + .{ "com.qa", {} }, + .{ "edu.qa", {} }, + .{ "gov.qa", {} }, + .{ "mil.qa", {} }, + .{ "name.qa", {} }, + .{ "net.qa", {} }, + .{ "org.qa", {} }, + .{ "sch.qa", {} }, + .{ "re", {} }, + .{ "asso.re", {} }, + .{ "com.re", {} }, + .{ "ro", {} }, + .{ "arts.ro", {} }, + .{ "com.ro", {} }, + .{ "firm.ro", {} }, + .{ "info.ro", {} }, + .{ "nom.ro", {} }, + .{ "nt.ro", {} }, + .{ "org.ro", {} }, + .{ "rec.ro", {} }, + .{ "store.ro", {} }, + .{ "tm.ro", {} }, + .{ "www.ro", {} }, + .{ "rs", {} }, + .{ "ac.rs", {} }, + .{ "co.rs", {} }, + .{ "edu.rs", {} }, + .{ "gov.rs", {} }, + .{ "in.rs", {} }, + .{ "org.rs", {} }, + .{ "ru", {} }, + .{ "rw", {} }, + .{ "ac.rw", {} }, + .{ "co.rw", {} }, + .{ "coop.rw", {} }, + .{ "gov.rw", {} }, + .{ "mil.rw", {} }, + .{ "net.rw", {} }, + .{ "org.rw", {} }, + .{ "sa", {} }, + .{ "com.sa", {} }, + .{ "edu.sa", {} }, + .{ "gov.sa", {} }, + .{ "med.sa", {} }, + .{ "net.sa", {} }, + .{ "org.sa", {} }, + .{ "pub.sa", {} }, + .{ "sch.sa", {} }, + .{ "sb", {} }, + .{ "com.sb", {} }, + .{ "edu.sb", {} }, + .{ "gov.sb", {} }, + .{ "net.sb", {} }, + .{ "org.sb", {} }, + .{ "sc", {} }, + .{ "com.sc", {} }, + .{ "edu.sc", {} }, + .{ "gov.sc", {} }, + .{ "net.sc", {} }, + .{ "org.sc", {} }, + .{ "sd", {} }, + .{ "com.sd", {} }, + .{ "edu.sd", {} }, + .{ "gov.sd", {} }, + .{ "info.sd", {} }, + .{ "med.sd", {} }, + .{ "net.sd", {} }, + .{ "org.sd", {} }, + .{ "tv.sd", {} }, + .{ "se", {} }, + .{ "a.se", {} }, + .{ "ac.se", {} }, + .{ "b.se", {} }, + .{ "bd.se", {} }, + .{ "brand.se", {} }, + .{ "c.se", {} }, + .{ "d.se", {} }, + .{ "e.se", {} }, + .{ "f.se", {} }, + .{ "fh.se", {} }, + .{ "fhsk.se", {} }, + .{ "fhv.se", {} }, + .{ "g.se", {} }, + .{ "h.se", {} }, + .{ "i.se", {} }, + .{ "k.se", {} }, + .{ "komforb.se", {} }, + .{ "kommunalforbund.se", {} }, + .{ "komvux.se", {} }, + .{ "l.se", {} }, + .{ "lanbib.se", {} }, + .{ "m.se", {} }, + .{ "n.se", {} }, + .{ "naturbruksgymn.se", {} }, + .{ "o.se", {} }, + .{ "org.se", {} }, + .{ "p.se", {} }, + .{ "parti.se", {} }, + .{ "pp.se", {} }, + .{ "press.se", {} }, + .{ "r.se", {} }, + .{ "s.se", {} }, + .{ "t.se", {} }, + .{ "tm.se", {} }, + .{ "u.se", {} }, + .{ "w.se", {} }, + .{ "x.se", {} }, + .{ "y.se", {} }, + .{ "z.se", {} }, + .{ "sg", {} }, + .{ "com.sg", {} }, + .{ "edu.sg", {} }, + .{ "gov.sg", {} }, + .{ "net.sg", {} }, + .{ "org.sg", {} }, + .{ "sh", {} }, + .{ "com.sh", {} }, + .{ "gov.sh", {} }, + .{ "mil.sh", {} }, + .{ "net.sh", {} }, + .{ "org.sh", {} }, + .{ "si", {} }, + .{ "sj", {} }, + .{ "sk", {} }, + .{ "sl", {} }, + .{ "com.sl", {} }, + .{ "edu.sl", {} }, + .{ "gov.sl", {} }, + .{ "net.sl", {} }, + .{ "org.sl", {} }, + .{ "sm", {} }, + .{ "sn", {} }, + .{ "art.sn", {} }, + .{ "com.sn", {} }, + .{ "edu.sn", {} }, + .{ "gouv.sn", {} }, + .{ "org.sn", {} }, + .{ "perso.sn", {} }, + .{ "univ.sn", {} }, + .{ "so", {} }, + .{ "com.so", {} }, + .{ "edu.so", {} }, + .{ "gov.so", {} }, + .{ "me.so", {} }, + .{ "net.so", {} }, + .{ "org.so", {} }, + .{ "sr", {} }, + .{ "ss", {} }, + .{ "biz.ss", {} }, + .{ "co.ss", {} }, + .{ "com.ss", {} }, + .{ "edu.ss", {} }, + .{ "gov.ss", {} }, + .{ "me.ss", {} }, + .{ "net.ss", {} }, + .{ "org.ss", {} }, + .{ "sch.ss", {} }, + .{ "st", {} }, + .{ "co.st", {} }, + .{ "com.st", {} }, + .{ "consulado.st", {} }, + .{ "edu.st", {} }, + .{ "embaixada.st", {} }, + .{ "mil.st", {} }, + .{ "net.st", {} }, + .{ "org.st", {} }, + .{ "principe.st", {} }, + .{ "saotome.st", {} }, + .{ "store.st", {} }, + .{ "su", {} }, + .{ "sv", {} }, + .{ "com.sv", {} }, + .{ "edu.sv", {} }, + .{ "gob.sv", {} }, + .{ "org.sv", {} }, + .{ "red.sv", {} }, + .{ "sx", {} }, + .{ "gov.sx", {} }, + .{ "sy", {} }, + .{ "com.sy", {} }, + .{ "edu.sy", {} }, + .{ "gov.sy", {} }, + .{ "mil.sy", {} }, + .{ "net.sy", {} }, + .{ "org.sy", {} }, + .{ "sz", {} }, + .{ "ac.sz", {} }, + .{ "co.sz", {} }, + .{ "org.sz", {} }, + .{ "tc", {} }, + .{ "td", {} }, + .{ "tel", {} }, + .{ "tf", {} }, + .{ "tg", {} }, + .{ "th", {} }, + .{ "ac.th", {} }, + .{ "co.th", {} }, + .{ "go.th", {} }, + .{ "in.th", {} }, + .{ "mi.th", {} }, + .{ "net.th", {} }, + .{ "or.th", {} }, + .{ "tj", {} }, + .{ "ac.tj", {} }, + .{ "biz.tj", {} }, + .{ "co.tj", {} }, + .{ "com.tj", {} }, + .{ "edu.tj", {} }, + .{ "go.tj", {} }, + .{ "gov.tj", {} }, + .{ "int.tj", {} }, + .{ "mil.tj", {} }, + .{ "name.tj", {} }, + .{ "net.tj", {} }, + .{ "nic.tj", {} }, + .{ "org.tj", {} }, + .{ "test.tj", {} }, + .{ "web.tj", {} }, + .{ "tk", {} }, + .{ "tl", {} }, + .{ "gov.tl", {} }, + .{ "tm", {} }, + .{ "co.tm", {} }, + .{ "com.tm", {} }, + .{ "edu.tm", {} }, + .{ "gov.tm", {} }, + .{ "mil.tm", {} }, + .{ "net.tm", {} }, + .{ "nom.tm", {} }, + .{ "org.tm", {} }, + .{ "tn", {} }, + .{ "com.tn", {} }, + .{ "ens.tn", {} }, + .{ "fin.tn", {} }, + .{ "gov.tn", {} }, + .{ "ind.tn", {} }, + .{ "info.tn", {} }, + .{ "intl.tn", {} }, + .{ "mincom.tn", {} }, + .{ "nat.tn", {} }, + .{ "net.tn", {} }, + .{ "org.tn", {} }, + .{ "perso.tn", {} }, + .{ "tourism.tn", {} }, + .{ "to", {} }, + .{ "com.to", {} }, + .{ "edu.to", {} }, + .{ "gov.to", {} }, + .{ "mil.to", {} }, + .{ "net.to", {} }, + .{ "org.to", {} }, + .{ "tr", {} }, + .{ "av.tr", {} }, + .{ "bbs.tr", {} }, + .{ "bel.tr", {} }, + .{ "biz.tr", {} }, + .{ "com.tr", {} }, + .{ "dr.tr", {} }, + .{ "edu.tr", {} }, + .{ "gen.tr", {} }, + .{ "gov.tr", {} }, + .{ "info.tr", {} }, + .{ "k12.tr", {} }, + .{ "kep.tr", {} }, + .{ "mil.tr", {} }, + .{ "name.tr", {} }, + .{ "net.tr", {} }, + .{ "org.tr", {} }, + .{ "pol.tr", {} }, + .{ "tel.tr", {} }, + .{ "tsk.tr", {} }, + .{ "tv.tr", {} }, + .{ "web.tr", {} }, + .{ "nc.tr", {} }, + .{ "gov.nc.tr", {} }, + .{ "tt", {} }, + .{ "biz.tt", {} }, + .{ "co.tt", {} }, + .{ "com.tt", {} }, + .{ "edu.tt", {} }, + .{ "gov.tt", {} }, + .{ "info.tt", {} }, + .{ "mil.tt", {} }, + .{ "name.tt", {} }, + .{ "net.tt", {} }, + .{ "org.tt", {} }, + .{ "pro.tt", {} }, + .{ "tv", {} }, + .{ "tw", {} }, + .{ "club.tw", {} }, + .{ "com.tw", {} }, + .{ "ebiz.tw", {} }, + .{ "edu.tw", {} }, + .{ "game.tw", {} }, + .{ "gov.tw", {} }, + .{ "idv.tw", {} }, + .{ "mil.tw", {} }, + .{ "net.tw", {} }, + .{ "org.tw", {} }, + .{ "tz", {} }, + .{ "ac.tz", {} }, + .{ "co.tz", {} }, + .{ "go.tz", {} }, + .{ "hotel.tz", {} }, + .{ "info.tz", {} }, + .{ "me.tz", {} }, + .{ "mil.tz", {} }, + .{ "mobi.tz", {} }, + .{ "ne.tz", {} }, + .{ "or.tz", {} }, + .{ "sc.tz", {} }, + .{ "tv.tz", {} }, + .{ "ua", {} }, + .{ "com.ua", {} }, + .{ "edu.ua", {} }, + .{ "gov.ua", {} }, + .{ "in.ua", {} }, + .{ "net.ua", {} }, + .{ "org.ua", {} }, + .{ "cherkassy.ua", {} }, + .{ "cherkasy.ua", {} }, + .{ "chernigov.ua", {} }, + .{ "chernihiv.ua", {} }, + .{ "chernivtsi.ua", {} }, + .{ "chernovtsy.ua", {} }, + .{ "ck.ua", {} }, + .{ "cn.ua", {} }, + .{ "cr.ua", {} }, + .{ "crimea.ua", {} }, + .{ "cv.ua", {} }, + .{ "dn.ua", {} }, + .{ "dnepropetrovsk.ua", {} }, + .{ "dnipropetrovsk.ua", {} }, + .{ "donetsk.ua", {} }, + .{ "dp.ua", {} }, + .{ "if.ua", {} }, + .{ "ivano-frankivsk.ua", {} }, + .{ "kh.ua", {} }, + .{ "kharkiv.ua", {} }, + .{ "kharkov.ua", {} }, + .{ "kherson.ua", {} }, + .{ "khmelnitskiy.ua", {} }, + .{ "khmelnytskyi.ua", {} }, + .{ "kiev.ua", {} }, + .{ "kirovograd.ua", {} }, + .{ "km.ua", {} }, + .{ "kr.ua", {} }, + .{ "kropyvnytskyi.ua", {} }, + .{ "krym.ua", {} }, + .{ "ks.ua", {} }, + .{ "kv.ua", {} }, + .{ "kyiv.ua", {} }, + .{ "lg.ua", {} }, + .{ "lt.ua", {} }, + .{ "lugansk.ua", {} }, + .{ "luhansk.ua", {} }, + .{ "lutsk.ua", {} }, + .{ "lv.ua", {} }, + .{ "lviv.ua", {} }, + .{ "mk.ua", {} }, + .{ "mykolaiv.ua", {} }, + .{ "nikolaev.ua", {} }, + .{ "od.ua", {} }, + .{ "odesa.ua", {} }, + .{ "odessa.ua", {} }, + .{ "pl.ua", {} }, + .{ "poltava.ua", {} }, + .{ "rivne.ua", {} }, + .{ "rovno.ua", {} }, + .{ "rv.ua", {} }, + .{ "sb.ua", {} }, + .{ "sebastopol.ua", {} }, + .{ "sevastopol.ua", {} }, + .{ "sm.ua", {} }, + .{ "sumy.ua", {} }, + .{ "te.ua", {} }, + .{ "ternopil.ua", {} }, + .{ "uz.ua", {} }, + .{ "uzhgorod.ua", {} }, + .{ "uzhhorod.ua", {} }, + .{ "vinnica.ua", {} }, + .{ "vinnytsia.ua", {} }, + .{ "vn.ua", {} }, + .{ "volyn.ua", {} }, + .{ "yalta.ua", {} }, + .{ "zakarpattia.ua", {} }, + .{ "zaporizhzhe.ua", {} }, + .{ "zaporizhzhia.ua", {} }, + .{ "zhitomir.ua", {} }, + .{ "zhytomyr.ua", {} }, + .{ "zp.ua", {} }, + .{ "zt.ua", {} }, + .{ "ug", {} }, + .{ "ac.ug", {} }, + .{ "co.ug", {} }, + .{ "com.ug", {} }, + .{ "edu.ug", {} }, + .{ "go.ug", {} }, + .{ "gov.ug", {} }, + .{ "mil.ug", {} }, + .{ "ne.ug", {} }, + .{ "or.ug", {} }, + .{ "org.ug", {} }, + .{ "sc.ug", {} }, + .{ "us.ug", {} }, + .{ "uk", {} }, + .{ "ac.uk", {} }, + .{ "co.uk", {} }, + .{ "gov.uk", {} }, + .{ "ltd.uk", {} }, + .{ "me.uk", {} }, + .{ "net.uk", {} }, + .{ "nhs.uk", {} }, + .{ "org.uk", {} }, + .{ "plc.uk", {} }, + .{ "police.uk", {} }, + .{ "*.sch.uk", {} }, + .{ "us", {} }, + .{ "dni.us", {} }, + .{ "isa.us", {} }, + .{ "nsn.us", {} }, + .{ "ak.us", {} }, + .{ "al.us", {} }, + .{ "ar.us", {} }, + .{ "as.us", {} }, + .{ "az.us", {} }, + .{ "ca.us", {} }, + .{ "co.us", {} }, + .{ "ct.us", {} }, + .{ "dc.us", {} }, + .{ "de.us", {} }, + .{ "fl.us", {} }, + .{ "ga.us", {} }, + .{ "gu.us", {} }, + .{ "hi.us", {} }, + .{ "ia.us", {} }, + .{ "id.us", {} }, + .{ "il.us", {} }, + .{ "in.us", {} }, + .{ "ks.us", {} }, + .{ "ky.us", {} }, + .{ "la.us", {} }, + .{ "ma.us", {} }, + .{ "md.us", {} }, + .{ "me.us", {} }, + .{ "mi.us", {} }, + .{ "mn.us", {} }, + .{ "mo.us", {} }, + .{ "ms.us", {} }, + .{ "mt.us", {} }, + .{ "nc.us", {} }, + .{ "nd.us", {} }, + .{ "ne.us", {} }, + .{ "nh.us", {} }, + .{ "nj.us", {} }, + .{ "nm.us", {} }, + .{ "nv.us", {} }, + .{ "ny.us", {} }, + .{ "oh.us", {} }, + .{ "ok.us", {} }, + .{ "or.us", {} }, + .{ "pa.us", {} }, + .{ "pr.us", {} }, + .{ "ri.us", {} }, + .{ "sc.us", {} }, + .{ "sd.us", {} }, + .{ "tn.us", {} }, + .{ "tx.us", {} }, + .{ "ut.us", {} }, + .{ "va.us", {} }, + .{ "vi.us", {} }, + .{ "vt.us", {} }, + .{ "wa.us", {} }, + .{ "wi.us", {} }, + .{ "wv.us", {} }, + .{ "wy.us", {} }, + .{ "k12.ak.us", {} }, + .{ "k12.al.us", {} }, + .{ "k12.ar.us", {} }, + .{ "k12.as.us", {} }, + .{ "k12.az.us", {} }, + .{ "k12.ca.us", {} }, + .{ "k12.co.us", {} }, + .{ "k12.ct.us", {} }, + .{ "k12.dc.us", {} }, + .{ "k12.fl.us", {} }, + .{ "k12.ga.us", {} }, + .{ "k12.gu.us", {} }, + .{ "k12.ia.us", {} }, + .{ "k12.id.us", {} }, + .{ "k12.il.us", {} }, + .{ "k12.in.us", {} }, + .{ "k12.ks.us", {} }, + .{ "k12.ky.us", {} }, + .{ "k12.la.us", {} }, + .{ "k12.ma.us", {} }, + .{ "k12.md.us", {} }, + .{ "k12.me.us", {} }, + .{ "k12.mi.us", {} }, + .{ "k12.mn.us", {} }, + .{ "k12.mo.us", {} }, + .{ "k12.ms.us", {} }, + .{ "k12.mt.us", {} }, + .{ "k12.nc.us", {} }, + .{ "k12.ne.us", {} }, + .{ "k12.nh.us", {} }, + .{ "k12.nj.us", {} }, + .{ "k12.nm.us", {} }, + .{ "k12.nv.us", {} }, + .{ "k12.ny.us", {} }, + .{ "k12.oh.us", {} }, + .{ "k12.ok.us", {} }, + .{ "k12.or.us", {} }, + .{ "k12.pa.us", {} }, + .{ "k12.pr.us", {} }, + .{ "k12.sc.us", {} }, + .{ "k12.tn.us", {} }, + .{ "k12.tx.us", {} }, + .{ "k12.ut.us", {} }, + .{ "k12.va.us", {} }, + .{ "k12.vi.us", {} }, + .{ "k12.vt.us", {} }, + .{ "k12.wa.us", {} }, + .{ "k12.wi.us", {} }, + .{ "cc.ak.us", {} }, + .{ "lib.ak.us", {} }, + .{ "cc.al.us", {} }, + .{ "lib.al.us", {} }, + .{ "cc.ar.us", {} }, + .{ "lib.ar.us", {} }, + .{ "cc.as.us", {} }, + .{ "lib.as.us", {} }, + .{ "cc.az.us", {} }, + .{ "lib.az.us", {} }, + .{ "cc.ca.us", {} }, + .{ "lib.ca.us", {} }, + .{ "cc.co.us", {} }, + .{ "lib.co.us", {} }, + .{ "cc.ct.us", {} }, + .{ "lib.ct.us", {} }, + .{ "cc.dc.us", {} }, + .{ "lib.dc.us", {} }, + .{ "cc.de.us", {} }, + .{ "cc.fl.us", {} }, + .{ "cc.ga.us", {} }, + .{ "cc.gu.us", {} }, + .{ "cc.hi.us", {} }, + .{ "cc.ia.us", {} }, + .{ "cc.id.us", {} }, + .{ "cc.il.us", {} }, + .{ "cc.in.us", {} }, + .{ "cc.ks.us", {} }, + .{ "cc.ky.us", {} }, + .{ "cc.la.us", {} }, + .{ "cc.ma.us", {} }, + .{ "cc.md.us", {} }, + .{ "cc.me.us", {} }, + .{ "cc.mi.us", {} }, + .{ "cc.mn.us", {} }, + .{ "cc.mo.us", {} }, + .{ "cc.ms.us", {} }, + .{ "cc.mt.us", {} }, + .{ "cc.nc.us", {} }, + .{ "cc.nd.us", {} }, + .{ "cc.ne.us", {} }, + .{ "cc.nh.us", {} }, + .{ "cc.nj.us", {} }, + .{ "cc.nm.us", {} }, + .{ "cc.nv.us", {} }, + .{ "cc.ny.us", {} }, + .{ "cc.oh.us", {} }, + .{ "cc.ok.us", {} }, + .{ "cc.or.us", {} }, + .{ "cc.pa.us", {} }, + .{ "cc.pr.us", {} }, + .{ "cc.ri.us", {} }, + .{ "cc.sc.us", {} }, + .{ "cc.sd.us", {} }, + .{ "cc.tn.us", {} }, + .{ "cc.tx.us", {} }, + .{ "cc.ut.us", {} }, + .{ "cc.va.us", {} }, + .{ "cc.vi.us", {} }, + .{ "cc.vt.us", {} }, + .{ "cc.wa.us", {} }, + .{ "cc.wi.us", {} }, + .{ "cc.wv.us", {} }, + .{ "cc.wy.us", {} }, + .{ "k12.wy.us", {} }, + .{ "lib.fl.us", {} }, + .{ "lib.ga.us", {} }, + .{ "lib.gu.us", {} }, + .{ "lib.hi.us", {} }, + .{ "lib.ia.us", {} }, + .{ "lib.id.us", {} }, + .{ "lib.il.us", {} }, + .{ "lib.in.us", {} }, + .{ "lib.ks.us", {} }, + .{ "lib.ky.us", {} }, + .{ "lib.la.us", {} }, + .{ "lib.ma.us", {} }, + .{ "lib.md.us", {} }, + .{ "lib.me.us", {} }, + .{ "lib.mi.us", {} }, + .{ "lib.mn.us", {} }, + .{ "lib.mo.us", {} }, + .{ "lib.ms.us", {} }, + .{ "lib.mt.us", {} }, + .{ "lib.nc.us", {} }, + .{ "lib.nd.us", {} }, + .{ "lib.ne.us", {} }, + .{ "lib.nh.us", {} }, + .{ "lib.nj.us", {} }, + .{ "lib.nm.us", {} }, + .{ "lib.nv.us", {} }, + .{ "lib.ny.us", {} }, + .{ "lib.oh.us", {} }, + .{ "lib.ok.us", {} }, + .{ "lib.or.us", {} }, + .{ "lib.pa.us", {} }, + .{ "lib.pr.us", {} }, + .{ "lib.ri.us", {} }, + .{ "lib.sc.us", {} }, + .{ "lib.sd.us", {} }, + .{ "lib.tn.us", {} }, + .{ "lib.tx.us", {} }, + .{ "lib.ut.us", {} }, + .{ "lib.va.us", {} }, + .{ "lib.vi.us", {} }, + .{ "lib.vt.us", {} }, + .{ "lib.wa.us", {} }, + .{ "lib.wi.us", {} }, + .{ "lib.wy.us", {} }, + .{ "chtr.k12.ma.us", {} }, + .{ "paroch.k12.ma.us", {} }, + .{ "pvt.k12.ma.us", {} }, + .{ "ann-arbor.mi.us", {} }, + .{ "cog.mi.us", {} }, + .{ "dst.mi.us", {} }, + .{ "eaton.mi.us", {} }, + .{ "gen.mi.us", {} }, + .{ "mus.mi.us", {} }, + .{ "tec.mi.us", {} }, + .{ "washtenaw.mi.us", {} }, + .{ "uy", {} }, + .{ "com.uy", {} }, + .{ "edu.uy", {} }, + .{ "gub.uy", {} }, + .{ "mil.uy", {} }, + .{ "net.uy", {} }, + .{ "org.uy", {} }, + .{ "uz", {} }, + .{ "co.uz", {} }, + .{ "com.uz", {} }, + .{ "net.uz", {} }, + .{ "org.uz", {} }, + .{ "va", {} }, + .{ "vc", {} }, + .{ "com.vc", {} }, + .{ "edu.vc", {} }, + .{ "gov.vc", {} }, + .{ "mil.vc", {} }, + .{ "net.vc", {} }, + .{ "org.vc", {} }, + .{ "ve", {} }, + .{ "arts.ve", {} }, + .{ "bib.ve", {} }, + .{ "co.ve", {} }, + .{ "com.ve", {} }, + .{ "e12.ve", {} }, + .{ "edu.ve", {} }, + .{ "emprende.ve", {} }, + .{ "firm.ve", {} }, + .{ "gob.ve", {} }, + .{ "gov.ve", {} }, + .{ "info.ve", {} }, + .{ "int.ve", {} }, + .{ "mil.ve", {} }, + .{ "net.ve", {} }, + .{ "nom.ve", {} }, + .{ "org.ve", {} }, + .{ "rar.ve", {} }, + .{ "rec.ve", {} }, + .{ "store.ve", {} }, + .{ "tec.ve", {} }, + .{ "web.ve", {} }, + .{ "vg", {} }, + .{ "edu.vg", {} }, + .{ "vi", {} }, + .{ "co.vi", {} }, + .{ "com.vi", {} }, + .{ "k12.vi", {} }, + .{ "net.vi", {} }, + .{ "org.vi", {} }, + .{ "vn", {} }, + .{ "ac.vn", {} }, + .{ "ai.vn", {} }, + .{ "biz.vn", {} }, + .{ "com.vn", {} }, + .{ "edu.vn", {} }, + .{ "gov.vn", {} }, + .{ "health.vn", {} }, + .{ "id.vn", {} }, + .{ "info.vn", {} }, + .{ "int.vn", {} }, + .{ "io.vn", {} }, + .{ "name.vn", {} }, + .{ "net.vn", {} }, + .{ "org.vn", {} }, + .{ "pro.vn", {} }, + .{ "angiang.vn", {} }, + .{ "bacgiang.vn", {} }, + .{ "backan.vn", {} }, + .{ "baclieu.vn", {} }, + .{ "bacninh.vn", {} }, + .{ "baria-vungtau.vn", {} }, + .{ "bentre.vn", {} }, + .{ "binhdinh.vn", {} }, + .{ "binhduong.vn", {} }, + .{ "binhphuoc.vn", {} }, + .{ "binhthuan.vn", {} }, + .{ "camau.vn", {} }, + .{ "cantho.vn", {} }, + .{ "caobang.vn", {} }, + .{ "daklak.vn", {} }, + .{ "daknong.vn", {} }, + .{ "danang.vn", {} }, + .{ "dienbien.vn", {} }, + .{ "dongnai.vn", {} }, + .{ "dongthap.vn", {} }, + .{ "gialai.vn", {} }, + .{ "hagiang.vn", {} }, + .{ "haiduong.vn", {} }, + .{ "haiphong.vn", {} }, + .{ "hanam.vn", {} }, + .{ "hanoi.vn", {} }, + .{ "hatinh.vn", {} }, + .{ "haugiang.vn", {} }, + .{ "hoabinh.vn", {} }, + .{ "hungyen.vn", {} }, + .{ "khanhhoa.vn", {} }, + .{ "kiengiang.vn", {} }, + .{ "kontum.vn", {} }, + .{ "laichau.vn", {} }, + .{ "lamdong.vn", {} }, + .{ "langson.vn", {} }, + .{ "laocai.vn", {} }, + .{ "longan.vn", {} }, + .{ "namdinh.vn", {} }, + .{ "nghean.vn", {} }, + .{ "ninhbinh.vn", {} }, + .{ "ninhthuan.vn", {} }, + .{ "phutho.vn", {} }, + .{ "phuyen.vn", {} }, + .{ "quangbinh.vn", {} }, + .{ "quangnam.vn", {} }, + .{ "quangngai.vn", {} }, + .{ "quangninh.vn", {} }, + .{ "quangtri.vn", {} }, + .{ "soctrang.vn", {} }, + .{ "sonla.vn", {} }, + .{ "tayninh.vn", {} }, + .{ "thaibinh.vn", {} }, + .{ "thainguyen.vn", {} }, + .{ "thanhhoa.vn", {} }, + .{ "thanhphohochiminh.vn", {} }, + .{ "thuathienhue.vn", {} }, + .{ "tiengiang.vn", {} }, + .{ "travinh.vn", {} }, + .{ "tuyenquang.vn", {} }, + .{ "vinhlong.vn", {} }, + .{ "vinhphuc.vn", {} }, + .{ "yenbai.vn", {} }, + .{ "vu", {} }, + .{ "com.vu", {} }, + .{ "edu.vu", {} }, + .{ "net.vu", {} }, + .{ "org.vu", {} }, + .{ "wf", {} }, + .{ "ws", {} }, + .{ "com.ws", {} }, + .{ "edu.ws", {} }, + .{ "gov.ws", {} }, + .{ "net.ws", {} }, + .{ "org.ws", {} }, + .{ "yt", {} }, + .{ "امارات", {} }, + .{ "հայ", {} }, + .{ "বাংলা", {} }, + .{ "бг", {} }, + .{ "البحرين", {} }, + .{ "бел", {} }, + .{ "中国", {} }, + .{ "中國", {} }, + .{ "الجزائر", {} }, + .{ "مصر", {} }, + .{ "ею", {} }, + .{ "ευ", {} }, + .{ "موريتانيا", {} }, + .{ "გე", {} }, + .{ "ελ", {} }, + .{ "香港", {} }, + .{ "個人.香港", {} }, + .{ "公司.香港", {} }, + .{ "政府.香港", {} }, + .{ "教育.香港", {} }, + .{ "組織.香港", {} }, + .{ "網絡.香港", {} }, + .{ "ಭಾರತ", {} }, + .{ "ଭାରତ", {} }, + .{ "ভাৰত", {} }, + .{ "भारतम्", {} }, + .{ "भारोत", {} }, + .{ "ڀارت", {} }, + .{ "ഭാരതം", {} }, + .{ "भारत", {} }, + .{ "بارت", {} }, + .{ "بھارت", {} }, + .{ "భారత్", {} }, + .{ "ભારત", {} }, + .{ "ਭਾਰਤ", {} }, + .{ "ভারত", {} }, + .{ "இந்தியா", {} }, + .{ "ایران", {} }, + .{ "ايران", {} }, + .{ "عراق", {} }, + .{ "الاردن", {} }, + .{ "한국", {} }, + .{ "қаз", {} }, + .{ "ລາວ", {} }, + .{ "ලංකා", {} }, + .{ "இலங்கை", {} }, + .{ "المغرب", {} }, + .{ "мкд", {} }, + .{ "мон", {} }, + .{ "澳門", {} }, + .{ "澳门", {} }, + .{ "مليسيا", {} }, + .{ "عمان", {} }, + .{ "پاکستان", {} }, + .{ "پاكستان", {} }, + .{ "فلسطين", {} }, + .{ "срб", {} }, + .{ "ак.срб", {} }, + .{ "обр.срб", {} }, + .{ "од.срб", {} }, + .{ "орг.срб", {} }, + .{ "пр.срб", {} }, + .{ "упр.срб", {} }, + .{ "рф", {} }, + .{ "قطر", {} }, + .{ "السعودية", {} }, + .{ "السعودیة", {} }, + .{ "السعودیۃ", {} }, + .{ "السعوديه", {} }, + .{ "سودان", {} }, + .{ "新加坡", {} }, + .{ "சிங்கப்பூர்", {} }, + .{ "سورية", {} }, + .{ "سوريا", {} }, + .{ "ไทย", {} }, + .{ "ทหาร.ไทย", {} }, + .{ "ธุรกิจ.ไทย", {} }, + .{ "เน็ต.ไทย", {} }, + .{ "รัฐบาล.ไทย", {} }, + .{ "ศึกษา.ไทย", {} }, + .{ "องค์กร.ไทย", {} }, + .{ "تونس", {} }, + .{ "台灣", {} }, + .{ "台湾", {} }, + .{ "臺灣", {} }, + .{ "укр", {} }, + .{ "اليمن", {} }, + .{ "xxx", {} }, + .{ "ye", {} }, + .{ "com.ye", {} }, + .{ "edu.ye", {} }, + .{ "gov.ye", {} }, + .{ "mil.ye", {} }, + .{ "net.ye", {} }, + .{ "org.ye", {} }, + .{ "ac.za", {} }, + .{ "agric.za", {} }, + .{ "alt.za", {} }, + .{ "co.za", {} }, + .{ "edu.za", {} }, + .{ "gov.za", {} }, + .{ "grondar.za", {} }, + .{ "law.za", {} }, + .{ "mil.za", {} }, + .{ "net.za", {} }, + .{ "ngo.za", {} }, + .{ "nic.za", {} }, + .{ "nis.za", {} }, + .{ "nom.za", {} }, + .{ "org.za", {} }, + .{ "school.za", {} }, + .{ "tm.za", {} }, + .{ "web.za", {} }, + .{ "zm", {} }, + .{ "ac.zm", {} }, + .{ "biz.zm", {} }, + .{ "co.zm", {} }, + .{ "com.zm", {} }, + .{ "edu.zm", {} }, + .{ "gov.zm", {} }, + .{ "info.zm", {} }, + .{ "mil.zm", {} }, + .{ "net.zm", {} }, + .{ "org.zm", {} }, + .{ "sch.zm", {} }, + .{ "zw", {} }, + .{ "ac.zw", {} }, + .{ "co.zw", {} }, + .{ "gov.zw", {} }, + .{ "mil.zw", {} }, + .{ "org.zw", {} }, + .{ "aaa", {} }, + .{ "aarp", {} }, + .{ "abb", {} }, + .{ "abbott", {} }, + .{ "abbvie", {} }, + .{ "abc", {} }, + .{ "able", {} }, + .{ "abogado", {} }, + .{ "abudhabi", {} }, + .{ "academy", {} }, + .{ "accenture", {} }, + .{ "accountant", {} }, + .{ "accountants", {} }, + .{ "aco", {} }, + .{ "actor", {} }, + .{ "ads", {} }, + .{ "adult", {} }, + .{ "aeg", {} }, + .{ "aetna", {} }, + .{ "afl", {} }, + .{ "africa", {} }, + .{ "agakhan", {} }, + .{ "agency", {} }, + .{ "aig", {} }, + .{ "airbus", {} }, + .{ "airforce", {} }, + .{ "airtel", {} }, + .{ "akdn", {} }, + .{ "alibaba", {} }, + .{ "alipay", {} }, + .{ "allfinanz", {} }, + .{ "allstate", {} }, + .{ "ally", {} }, + .{ "alsace", {} }, + .{ "alstom", {} }, + .{ "amazon", {} }, + .{ "americanexpress", {} }, + .{ "americanfamily", {} }, + .{ "amex", {} }, + .{ "amfam", {} }, + .{ "amica", {} }, + .{ "amsterdam", {} }, + .{ "analytics", {} }, + .{ "android", {} }, + .{ "anquan", {} }, + .{ "anz", {} }, + .{ "aol", {} }, + .{ "apartments", {} }, + .{ "app", {} }, + .{ "apple", {} }, + .{ "aquarelle", {} }, + .{ "arab", {} }, + .{ "aramco", {} }, + .{ "archi", {} }, + .{ "army", {} }, + .{ "art", {} }, + .{ "arte", {} }, + .{ "asda", {} }, + .{ "associates", {} }, + .{ "athleta", {} }, + .{ "attorney", {} }, + .{ "auction", {} }, + .{ "audi", {} }, + .{ "audible", {} }, + .{ "audio", {} }, + .{ "auspost", {} }, + .{ "author", {} }, + .{ "auto", {} }, + .{ "autos", {} }, + .{ "aws", {} }, + .{ "axa", {} }, + .{ "azure", {} }, + .{ "baby", {} }, + .{ "baidu", {} }, + .{ "banamex", {} }, + .{ "band", {} }, + .{ "bank", {} }, + .{ "bar", {} }, + .{ "barcelona", {} }, + .{ "barclaycard", {} }, + .{ "barclays", {} }, + .{ "barefoot", {} }, + .{ "bargains", {} }, + .{ "baseball", {} }, + .{ "basketball", {} }, + .{ "bauhaus", {} }, + .{ "bayern", {} }, + .{ "bbc", {} }, + .{ "bbt", {} }, + .{ "bbva", {} }, + .{ "bcg", {} }, + .{ "bcn", {} }, + .{ "beats", {} }, + .{ "beauty", {} }, + .{ "beer", {} }, + .{ "bentley", {} }, + .{ "berlin", {} }, + .{ "best", {} }, + .{ "bestbuy", {} }, + .{ "bet", {} }, + .{ "bharti", {} }, + .{ "bible", {} }, + .{ "bid", {} }, + .{ "bike", {} }, + .{ "bing", {} }, + .{ "bingo", {} }, + .{ "bio", {} }, + .{ "black", {} }, + .{ "blackfriday", {} }, + .{ "blockbuster", {} }, + .{ "blog", {} }, + .{ "bloomberg", {} }, + .{ "blue", {} }, + .{ "bms", {} }, + .{ "bmw", {} }, + .{ "bnpparibas", {} }, + .{ "boats", {} }, + .{ "boehringer", {} }, + .{ "bofa", {} }, + .{ "bom", {} }, + .{ "bond", {} }, + .{ "boo", {} }, + .{ "book", {} }, + .{ "booking", {} }, + .{ "bosch", {} }, + .{ "bostik", {} }, + .{ "boston", {} }, + .{ "bot", {} }, + .{ "boutique", {} }, + .{ "box", {} }, + .{ "bradesco", {} }, + .{ "bridgestone", {} }, + .{ "broadway", {} }, + .{ "broker", {} }, + .{ "brother", {} }, + .{ "brussels", {} }, + .{ "build", {} }, + .{ "builders", {} }, + .{ "business", {} }, + .{ "buy", {} }, + .{ "buzz", {} }, + .{ "bzh", {} }, + .{ "cab", {} }, + .{ "cafe", {} }, + .{ "cal", {} }, + .{ "call", {} }, + .{ "calvinklein", {} }, + .{ "cam", {} }, + .{ "camera", {} }, + .{ "camp", {} }, + .{ "canon", {} }, + .{ "capetown", {} }, + .{ "capital", {} }, + .{ "capitalone", {} }, + .{ "car", {} }, + .{ "caravan", {} }, + .{ "cards", {} }, + .{ "care", {} }, + .{ "career", {} }, + .{ "careers", {} }, + .{ "cars", {} }, + .{ "casa", {} }, + .{ "case", {} }, + .{ "cash", {} }, + .{ "casino", {} }, + .{ "catering", {} }, + .{ "catholic", {} }, + .{ "cba", {} }, + .{ "cbn", {} }, + .{ "cbre", {} }, + .{ "center", {} }, + .{ "ceo", {} }, + .{ "cern", {} }, + .{ "cfa", {} }, + .{ "cfd", {} }, + .{ "chanel", {} }, + .{ "channel", {} }, + .{ "charity", {} }, + .{ "chase", {} }, + .{ "chat", {} }, + .{ "cheap", {} }, + .{ "chintai", {} }, + .{ "christmas", {} }, + .{ "chrome", {} }, + .{ "church", {} }, + .{ "cipriani", {} }, + .{ "circle", {} }, + .{ "cisco", {} }, + .{ "citadel", {} }, + .{ "citi", {} }, + .{ "citic", {} }, + .{ "city", {} }, + .{ "claims", {} }, + .{ "cleaning", {} }, + .{ "click", {} }, + .{ "clinic", {} }, + .{ "clinique", {} }, + .{ "clothing", {} }, + .{ "cloud", {} }, + .{ "club", {} }, + .{ "clubmed", {} }, + .{ "coach", {} }, + .{ "codes", {} }, + .{ "coffee", {} }, + .{ "college", {} }, + .{ "cologne", {} }, + .{ "commbank", {} }, + .{ "community", {} }, + .{ "company", {} }, + .{ "compare", {} }, + .{ "computer", {} }, + .{ "comsec", {} }, + .{ "condos", {} }, + .{ "construction", {} }, + .{ "consulting", {} }, + .{ "contact", {} }, + .{ "contractors", {} }, + .{ "cooking", {} }, + .{ "cool", {} }, + .{ "corsica", {} }, + .{ "country", {} }, + .{ "coupon", {} }, + .{ "coupons", {} }, + .{ "courses", {} }, + .{ "cpa", {} }, + .{ "credit", {} }, + .{ "creditcard", {} }, + .{ "creditunion", {} }, + .{ "cricket", {} }, + .{ "crown", {} }, + .{ "crs", {} }, + .{ "cruise", {} }, + .{ "cruises", {} }, + .{ "cuisinella", {} }, + .{ "cymru", {} }, + .{ "cyou", {} }, + .{ "dad", {} }, + .{ "dance", {} }, + .{ "data", {} }, + .{ "date", {} }, + .{ "dating", {} }, + .{ "datsun", {} }, + .{ "day", {} }, + .{ "dclk", {} }, + .{ "dds", {} }, + .{ "deal", {} }, + .{ "dealer", {} }, + .{ "deals", {} }, + .{ "degree", {} }, + .{ "delivery", {} }, + .{ "dell", {} }, + .{ "deloitte", {} }, + .{ "delta", {} }, + .{ "democrat", {} }, + .{ "dental", {} }, + .{ "dentist", {} }, + .{ "desi", {} }, + .{ "design", {} }, + .{ "dev", {} }, + .{ "dhl", {} }, + .{ "diamonds", {} }, + .{ "diet", {} }, + .{ "digital", {} }, + .{ "direct", {} }, + .{ "directory", {} }, + .{ "discount", {} }, + .{ "discover", {} }, + .{ "dish", {} }, + .{ "diy", {} }, + .{ "dnp", {} }, + .{ "docs", {} }, + .{ "doctor", {} }, + .{ "dog", {} }, + .{ "domains", {} }, + .{ "dot", {} }, + .{ "download", {} }, + .{ "drive", {} }, + .{ "dtv", {} }, + .{ "dubai", {} }, + .{ "dunlop", {} }, + .{ "dupont", {} }, + .{ "durban", {} }, + .{ "dvag", {} }, + .{ "dvr", {} }, + .{ "earth", {} }, + .{ "eat", {} }, + .{ "eco", {} }, + .{ "edeka", {} }, + .{ "education", {} }, + .{ "email", {} }, + .{ "emerck", {} }, + .{ "energy", {} }, + .{ "engineer", {} }, + .{ "engineering", {} }, + .{ "enterprises", {} }, + .{ "epson", {} }, + .{ "equipment", {} }, + .{ "ericsson", {} }, + .{ "erni", {} }, + .{ "esq", {} }, + .{ "estate", {} }, + .{ "eurovision", {} }, + .{ "eus", {} }, + .{ "events", {} }, + .{ "exchange", {} }, + .{ "expert", {} }, + .{ "exposed", {} }, + .{ "express", {} }, + .{ "extraspace", {} }, + .{ "fage", {} }, + .{ "fail", {} }, + .{ "fairwinds", {} }, + .{ "faith", {} }, + .{ "family", {} }, + .{ "fan", {} }, + .{ "fans", {} }, + .{ "farm", {} }, + .{ "farmers", {} }, + .{ "fashion", {} }, + .{ "fast", {} }, + .{ "fedex", {} }, + .{ "feedback", {} }, + .{ "ferrari", {} }, + .{ "ferrero", {} }, + .{ "fidelity", {} }, + .{ "fido", {} }, + .{ "film", {} }, + .{ "final", {} }, + .{ "finance", {} }, + .{ "financial", {} }, + .{ "fire", {} }, + .{ "firestone", {} }, + .{ "firmdale", {} }, + .{ "fish", {} }, + .{ "fishing", {} }, + .{ "fit", {} }, + .{ "fitness", {} }, + .{ "flickr", {} }, + .{ "flights", {} }, + .{ "flir", {} }, + .{ "florist", {} }, + .{ "flowers", {} }, + .{ "fly", {} }, + .{ "foo", {} }, + .{ "food", {} }, + .{ "football", {} }, + .{ "ford", {} }, + .{ "forex", {} }, + .{ "forsale", {} }, + .{ "forum", {} }, + .{ "foundation", {} }, + .{ "fox", {} }, + .{ "free", {} }, + .{ "fresenius", {} }, + .{ "frl", {} }, + .{ "frogans", {} }, + .{ "frontier", {} }, + .{ "ftr", {} }, + .{ "fujitsu", {} }, + .{ "fun", {} }, + .{ "fund", {} }, + .{ "furniture", {} }, + .{ "futbol", {} }, + .{ "fyi", {} }, + .{ "gal", {} }, + .{ "gallery", {} }, + .{ "gallo", {} }, + .{ "gallup", {} }, + .{ "game", {} }, + .{ "games", {} }, + .{ "gap", {} }, + .{ "garden", {} }, + .{ "gay", {} }, + .{ "gbiz", {} }, + .{ "gdn", {} }, + .{ "gea", {} }, + .{ "gent", {} }, + .{ "genting", {} }, + .{ "george", {} }, + .{ "ggee", {} }, + .{ "gift", {} }, + .{ "gifts", {} }, + .{ "gives", {} }, + .{ "giving", {} }, + .{ "glass", {} }, + .{ "gle", {} }, + .{ "global", {} }, + .{ "globo", {} }, + .{ "gmail", {} }, + .{ "gmbh", {} }, + .{ "gmo", {} }, + .{ "gmx", {} }, + .{ "godaddy", {} }, + .{ "gold", {} }, + .{ "goldpoint", {} }, + .{ "golf", {} }, + .{ "goo", {} }, + .{ "goodyear", {} }, + .{ "goog", {} }, + .{ "google", {} }, + .{ "gop", {} }, + .{ "got", {} }, + .{ "grainger", {} }, + .{ "graphics", {} }, + .{ "gratis", {} }, + .{ "green", {} }, + .{ "gripe", {} }, + .{ "grocery", {} }, + .{ "group", {} }, + .{ "gucci", {} }, + .{ "guge", {} }, + .{ "guide", {} }, + .{ "guitars", {} }, + .{ "guru", {} }, + .{ "hair", {} }, + .{ "hamburg", {} }, + .{ "hangout", {} }, + .{ "haus", {} }, + .{ "hbo", {} }, + .{ "hdfc", {} }, + .{ "hdfcbank", {} }, + .{ "health", {} }, + .{ "healthcare", {} }, + .{ "help", {} }, + .{ "helsinki", {} }, + .{ "here", {} }, + .{ "hermes", {} }, + .{ "hiphop", {} }, + .{ "hisamitsu", {} }, + .{ "hitachi", {} }, + .{ "hiv", {} }, + .{ "hkt", {} }, + .{ "hockey", {} }, + .{ "holdings", {} }, + .{ "holiday", {} }, + .{ "homedepot", {} }, + .{ "homegoods", {} }, + .{ "homes", {} }, + .{ "homesense", {} }, + .{ "honda", {} }, + .{ "horse", {} }, + .{ "hospital", {} }, + .{ "host", {} }, + .{ "hosting", {} }, + .{ "hot", {} }, + .{ "hotels", {} }, + .{ "hotmail", {} }, + .{ "house", {} }, + .{ "how", {} }, + .{ "hsbc", {} }, + .{ "hughes", {} }, + .{ "hyatt", {} }, + .{ "hyundai", {} }, + .{ "ibm", {} }, + .{ "icbc", {} }, + .{ "ice", {} }, + .{ "icu", {} }, + .{ "ieee", {} }, + .{ "ifm", {} }, + .{ "ikano", {} }, + .{ "imamat", {} }, + .{ "imdb", {} }, + .{ "immo", {} }, + .{ "immobilien", {} }, + .{ "inc", {} }, + .{ "industries", {} }, + .{ "infiniti", {} }, + .{ "ing", {} }, + .{ "ink", {} }, + .{ "institute", {} }, + .{ "insurance", {} }, + .{ "insure", {} }, + .{ "international", {} }, + .{ "intuit", {} }, + .{ "investments", {} }, + .{ "ipiranga", {} }, + .{ "irish", {} }, + .{ "ismaili", {} }, + .{ "ist", {} }, + .{ "istanbul", {} }, + .{ "itau", {} }, + .{ "itv", {} }, + .{ "jaguar", {} }, + .{ "java", {} }, + .{ "jcb", {} }, + .{ "jeep", {} }, + .{ "jetzt", {} }, + .{ "jewelry", {} }, + .{ "jio", {} }, + .{ "jll", {} }, + .{ "jmp", {} }, + .{ "jnj", {} }, + .{ "joburg", {} }, + .{ "jot", {} }, + .{ "joy", {} }, + .{ "jpmorgan", {} }, + .{ "jprs", {} }, + .{ "juegos", {} }, + .{ "juniper", {} }, + .{ "kaufen", {} }, + .{ "kddi", {} }, + .{ "kerryhotels", {} }, + .{ "kerrylogistics", {} }, + .{ "kerryproperties", {} }, + .{ "kfh", {} }, + .{ "kia", {} }, + .{ "kids", {} }, + .{ "kim", {} }, + .{ "kindle", {} }, + .{ "kitchen", {} }, + .{ "kiwi", {} }, + .{ "koeln", {} }, + .{ "komatsu", {} }, + .{ "kosher", {} }, + .{ "kpmg", {} }, + .{ "kpn", {} }, + .{ "krd", {} }, + .{ "kred", {} }, + .{ "kuokgroup", {} }, + .{ "kyoto", {} }, + .{ "lacaixa", {} }, + .{ "lamborghini", {} }, + .{ "lamer", {} }, + .{ "lancaster", {} }, + .{ "land", {} }, + .{ "landrover", {} }, + .{ "lanxess", {} }, + .{ "lasalle", {} }, + .{ "lat", {} }, + .{ "latino", {} }, + .{ "latrobe", {} }, + .{ "law", {} }, + .{ "lawyer", {} }, + .{ "lds", {} }, + .{ "lease", {} }, + .{ "leclerc", {} }, + .{ "lefrak", {} }, + .{ "legal", {} }, + .{ "lego", {} }, + .{ "lexus", {} }, + .{ "lgbt", {} }, + .{ "lidl", {} }, + .{ "life", {} }, + .{ "lifeinsurance", {} }, + .{ "lifestyle", {} }, + .{ "lighting", {} }, + .{ "like", {} }, + .{ "lilly", {} }, + .{ "limited", {} }, + .{ "limo", {} }, + .{ "lincoln", {} }, + .{ "link", {} }, + .{ "lipsy", {} }, + .{ "live", {} }, + .{ "living", {} }, + .{ "llc", {} }, + .{ "llp", {} }, + .{ "loan", {} }, + .{ "loans", {} }, + .{ "locker", {} }, + .{ "locus", {} }, + .{ "lol", {} }, + .{ "london", {} }, + .{ "lotte", {} }, + .{ "lotto", {} }, + .{ "love", {} }, + .{ "lpl", {} }, + .{ "lplfinancial", {} }, + .{ "ltd", {} }, + .{ "ltda", {} }, + .{ "lundbeck", {} }, + .{ "luxe", {} }, + .{ "luxury", {} }, + .{ "madrid", {} }, + .{ "maif", {} }, + .{ "maison", {} }, + .{ "makeup", {} }, + .{ "man", {} }, + .{ "management", {} }, + .{ "mango", {} }, + .{ "map", {} }, + .{ "market", {} }, + .{ "marketing", {} }, + .{ "markets", {} }, + .{ "marriott", {} }, + .{ "marshalls", {} }, + .{ "mattel", {} }, + .{ "mba", {} }, + .{ "mckinsey", {} }, + .{ "med", {} }, + .{ "media", {} }, + .{ "meet", {} }, + .{ "melbourne", {} }, + .{ "meme", {} }, + .{ "memorial", {} }, + .{ "men", {} }, + .{ "menu", {} }, + .{ "merck", {} }, + .{ "merckmsd", {} }, + .{ "miami", {} }, + .{ "microsoft", {} }, + .{ "mini", {} }, + .{ "mint", {} }, + .{ "mit", {} }, + .{ "mitsubishi", {} }, + .{ "mlb", {} }, + .{ "mls", {} }, + .{ "mma", {} }, + .{ "mobile", {} }, + .{ "moda", {} }, + .{ "moe", {} }, + .{ "moi", {} }, + .{ "mom", {} }, + .{ "monash", {} }, + .{ "money", {} }, + .{ "monster", {} }, + .{ "mormon", {} }, + .{ "mortgage", {} }, + .{ "moscow", {} }, + .{ "moto", {} }, + .{ "motorcycles", {} }, + .{ "mov", {} }, + .{ "movie", {} }, + .{ "msd", {} }, + .{ "mtn", {} }, + .{ "mtr", {} }, + .{ "music", {} }, + .{ "nab", {} }, + .{ "nagoya", {} }, + .{ "navy", {} }, + .{ "nba", {} }, + .{ "nec", {} }, + .{ "netbank", {} }, + .{ "netflix", {} }, + .{ "network", {} }, + .{ "neustar", {} }, + .{ "new", {} }, + .{ "news", {} }, + .{ "next", {} }, + .{ "nextdirect", {} }, + .{ "nexus", {} }, + .{ "nfl", {} }, + .{ "ngo", {} }, + .{ "nhk", {} }, + .{ "nico", {} }, + .{ "nike", {} }, + .{ "nikon", {} }, + .{ "ninja", {} }, + .{ "nissan", {} }, + .{ "nissay", {} }, + .{ "nokia", {} }, + .{ "norton", {} }, + .{ "now", {} }, + .{ "nowruz", {} }, + .{ "nowtv", {} }, + .{ "nra", {} }, + .{ "nrw", {} }, + .{ "ntt", {} }, + .{ "nyc", {} }, + .{ "obi", {} }, + .{ "observer", {} }, + .{ "office", {} }, + .{ "okinawa", {} }, + .{ "olayan", {} }, + .{ "olayangroup", {} }, + .{ "ollo", {} }, + .{ "omega", {} }, + .{ "one", {} }, + .{ "ong", {} }, + .{ "onl", {} }, + .{ "online", {} }, + .{ "ooo", {} }, + .{ "open", {} }, + .{ "oracle", {} }, + .{ "orange", {} }, + .{ "organic", {} }, + .{ "origins", {} }, + .{ "osaka", {} }, + .{ "otsuka", {} }, + .{ "ott", {} }, + .{ "ovh", {} }, + .{ "page", {} }, + .{ "panasonic", {} }, + .{ "paris", {} }, + .{ "pars", {} }, + .{ "partners", {} }, + .{ "parts", {} }, + .{ "party", {} }, + .{ "pay", {} }, + .{ "pccw", {} }, + .{ "pet", {} }, + .{ "pfizer", {} }, + .{ "pharmacy", {} }, + .{ "phd", {} }, + .{ "philips", {} }, + .{ "phone", {} }, + .{ "photo", {} }, + .{ "photography", {} }, + .{ "photos", {} }, + .{ "physio", {} }, + .{ "pics", {} }, + .{ "pictet", {} }, + .{ "pictures", {} }, + .{ "pid", {} }, + .{ "pin", {} }, + .{ "ping", {} }, + .{ "pink", {} }, + .{ "pioneer", {} }, + .{ "pizza", {} }, + .{ "place", {} }, + .{ "play", {} }, + .{ "playstation", {} }, + .{ "plumbing", {} }, + .{ "plus", {} }, + .{ "pnc", {} }, + .{ "pohl", {} }, + .{ "poker", {} }, + .{ "politie", {} }, + .{ "porn", {} }, + .{ "pramerica", {} }, + .{ "praxi", {} }, + .{ "press", {} }, + .{ "prime", {} }, + .{ "prod", {} }, + .{ "productions", {} }, + .{ "prof", {} }, + .{ "progressive", {} }, + .{ "promo", {} }, + .{ "properties", {} }, + .{ "property", {} }, + .{ "protection", {} }, + .{ "pru", {} }, + .{ "prudential", {} }, + .{ "pub", {} }, + .{ "pwc", {} }, + .{ "qpon", {} }, + .{ "quebec", {} }, + .{ "quest", {} }, + .{ "racing", {} }, + .{ "radio", {} }, + .{ "read", {} }, + .{ "realestate", {} }, + .{ "realtor", {} }, + .{ "realty", {} }, + .{ "recipes", {} }, + .{ "red", {} }, + .{ "redstone", {} }, + .{ "redumbrella", {} }, + .{ "rehab", {} }, + .{ "reise", {} }, + .{ "reisen", {} }, + .{ "reit", {} }, + .{ "reliance", {} }, + .{ "ren", {} }, + .{ "rent", {} }, + .{ "rentals", {} }, + .{ "repair", {} }, + .{ "report", {} }, + .{ "republican", {} }, + .{ "rest", {} }, + .{ "restaurant", {} }, + .{ "review", {} }, + .{ "reviews", {} }, + .{ "rexroth", {} }, + .{ "rich", {} }, + .{ "richardli", {} }, + .{ "ricoh", {} }, + .{ "ril", {} }, + .{ "rio", {} }, + .{ "rip", {} }, + .{ "rocks", {} }, + .{ "rodeo", {} }, + .{ "rogers", {} }, + .{ "room", {} }, + .{ "rsvp", {} }, + .{ "rugby", {} }, + .{ "ruhr", {} }, + .{ "run", {} }, + .{ "rwe", {} }, + .{ "ryukyu", {} }, + .{ "saarland", {} }, + .{ "safe", {} }, + .{ "safety", {} }, + .{ "sakura", {} }, + .{ "sale", {} }, + .{ "salon", {} }, + .{ "samsclub", {} }, + .{ "samsung", {} }, + .{ "sandvik", {} }, + .{ "sandvikcoromant", {} }, + .{ "sanofi", {} }, + .{ "sap", {} }, + .{ "sarl", {} }, + .{ "sas", {} }, + .{ "save", {} }, + .{ "saxo", {} }, + .{ "sbi", {} }, + .{ "sbs", {} }, + .{ "scb", {} }, + .{ "schaeffler", {} }, + .{ "schmidt", {} }, + .{ "scholarships", {} }, + .{ "school", {} }, + .{ "schule", {} }, + .{ "schwarz", {} }, + .{ "science", {} }, + .{ "scot", {} }, + .{ "search", {} }, + .{ "seat", {} }, + .{ "secure", {} }, + .{ "security", {} }, + .{ "seek", {} }, + .{ "select", {} }, + .{ "sener", {} }, + .{ "services", {} }, + .{ "seven", {} }, + .{ "sew", {} }, + .{ "sex", {} }, + .{ "sexy", {} }, + .{ "sfr", {} }, + .{ "shangrila", {} }, + .{ "sharp", {} }, + .{ "shell", {} }, + .{ "shia", {} }, + .{ "shiksha", {} }, + .{ "shoes", {} }, + .{ "shop", {} }, + .{ "shopping", {} }, + .{ "shouji", {} }, + .{ "show", {} }, + .{ "silk", {} }, + .{ "sina", {} }, + .{ "singles", {} }, + .{ "site", {} }, + .{ "ski", {} }, + .{ "skin", {} }, + .{ "sky", {} }, + .{ "skype", {} }, + .{ "sling", {} }, + .{ "smart", {} }, + .{ "smile", {} }, + .{ "sncf", {} }, + .{ "soccer", {} }, + .{ "social", {} }, + .{ "softbank", {} }, + .{ "software", {} }, + .{ "sohu", {} }, + .{ "solar", {} }, + .{ "solutions", {} }, + .{ "song", {} }, + .{ "sony", {} }, + .{ "soy", {} }, + .{ "spa", {} }, + .{ "space", {} }, + .{ "sport", {} }, + .{ "spot", {} }, + .{ "srl", {} }, + .{ "stada", {} }, + .{ "staples", {} }, + .{ "star", {} }, + .{ "statebank", {} }, + .{ "statefarm", {} }, + .{ "stc", {} }, + .{ "stcgroup", {} }, + .{ "stockholm", {} }, + .{ "storage", {} }, + .{ "store", {} }, + .{ "stream", {} }, + .{ "studio", {} }, + .{ "study", {} }, + .{ "style", {} }, + .{ "sucks", {} }, + .{ "supplies", {} }, + .{ "supply", {} }, + .{ "support", {} }, + .{ "surf", {} }, + .{ "surgery", {} }, + .{ "suzuki", {} }, + .{ "swatch", {} }, + .{ "swiss", {} }, + .{ "sydney", {} }, + .{ "systems", {} }, + .{ "tab", {} }, + .{ "taipei", {} }, + .{ "talk", {} }, + .{ "taobao", {} }, + .{ "target", {} }, + .{ "tatamotors", {} }, + .{ "tatar", {} }, + .{ "tattoo", {} }, + .{ "tax", {} }, + .{ "taxi", {} }, + .{ "tci", {} }, + .{ "tdk", {} }, + .{ "team", {} }, + .{ "tech", {} }, + .{ "technology", {} }, + .{ "temasek", {} }, + .{ "tennis", {} }, + .{ "teva", {} }, + .{ "thd", {} }, + .{ "theater", {} }, + .{ "theatre", {} }, + .{ "tiaa", {} }, + .{ "tickets", {} }, + .{ "tienda", {} }, + .{ "tips", {} }, + .{ "tires", {} }, + .{ "tirol", {} }, + .{ "tjmaxx", {} }, + .{ "tjx", {} }, + .{ "tkmaxx", {} }, + .{ "tmall", {} }, + .{ "today", {} }, + .{ "tokyo", {} }, + .{ "tools", {} }, + .{ "top", {} }, + .{ "toray", {} }, + .{ "toshiba", {} }, + .{ "total", {} }, + .{ "tours", {} }, + .{ "town", {} }, + .{ "toyota", {} }, + .{ "toys", {} }, + .{ "trade", {} }, + .{ "trading", {} }, + .{ "training", {} }, + .{ "travel", {} }, + .{ "travelers", {} }, + .{ "travelersinsurance", {} }, + .{ "trust", {} }, + .{ "trv", {} }, + .{ "tube", {} }, + .{ "tui", {} }, + .{ "tunes", {} }, + .{ "tushu", {} }, + .{ "tvs", {} }, + .{ "ubank", {} }, + .{ "ubs", {} }, + .{ "unicom", {} }, + .{ "university", {} }, + .{ "uno", {} }, + .{ "uol", {} }, + .{ "ups", {} }, + .{ "vacations", {} }, + .{ "vana", {} }, + .{ "vanguard", {} }, + .{ "vegas", {} }, + .{ "ventures", {} }, + .{ "verisign", {} }, + .{ "versicherung", {} }, + .{ "vet", {} }, + .{ "viajes", {} }, + .{ "video", {} }, + .{ "vig", {} }, + .{ "viking", {} }, + .{ "villas", {} }, + .{ "vin", {} }, + .{ "vip", {} }, + .{ "virgin", {} }, + .{ "visa", {} }, + .{ "vision", {} }, + .{ "viva", {} }, + .{ "vivo", {} }, + .{ "vlaanderen", {} }, + .{ "vodka", {} }, + .{ "volvo", {} }, + .{ "vote", {} }, + .{ "voting", {} }, + .{ "voto", {} }, + .{ "voyage", {} }, + .{ "wales", {} }, + .{ "walmart", {} }, + .{ "walter", {} }, + .{ "wang", {} }, + .{ "wanggou", {} }, + .{ "watch", {} }, + .{ "watches", {} }, + .{ "weather", {} }, + .{ "weatherchannel", {} }, + .{ "webcam", {} }, + .{ "weber", {} }, + .{ "website", {} }, + .{ "wed", {} }, + .{ "wedding", {} }, + .{ "weibo", {} }, + .{ "weir", {} }, + .{ "whoswho", {} }, + .{ "wien", {} }, + .{ "wiki", {} }, + .{ "williamhill", {} }, + .{ "win", {} }, + .{ "windows", {} }, + .{ "wine", {} }, + .{ "winners", {} }, + .{ "wme", {} }, + .{ "wolterskluwer", {} }, + .{ "woodside", {} }, + .{ "work", {} }, + .{ "works", {} }, + .{ "world", {} }, + .{ "wow", {} }, + .{ "wtc", {} }, + .{ "wtf", {} }, + .{ "xbox", {} }, + .{ "xerox", {} }, + .{ "xihuan", {} }, + .{ "xin", {} }, + .{ "कॉम", {} }, + .{ "セール", {} }, + .{ "佛山", {} }, + .{ "慈善", {} }, + .{ "集团", {} }, + .{ "在线", {} }, + .{ "点看", {} }, + .{ "คอม", {} }, + .{ "八卦", {} }, + .{ "موقع", {} }, + .{ "公益", {} }, + .{ "公司", {} }, + .{ "香格里拉", {} }, + .{ "网站", {} }, + .{ "移动", {} }, + .{ "我爱你", {} }, + .{ "москва", {} }, + .{ "католик", {} }, + .{ "онлайн", {} }, + .{ "сайт", {} }, + .{ "联通", {} }, + .{ "קום", {} }, + .{ "时尚", {} }, + .{ "微博", {} }, + .{ "淡马锡", {} }, + .{ "ファッション", {} }, + .{ "орг", {} }, + .{ "नेट", {} }, + .{ "ストア", {} }, + .{ "アマゾン", {} }, + .{ "삼성", {} }, + .{ "商标", {} }, + .{ "商店", {} }, + .{ "商城", {} }, + .{ "дети", {} }, + .{ "ポイント", {} }, + .{ "新闻", {} }, + .{ "家電", {} }, + .{ "كوم", {} }, + .{ "中文网", {} }, + .{ "中信", {} }, + .{ "娱乐", {} }, + .{ "谷歌", {} }, + .{ "電訊盈科", {} }, + .{ "购物", {} }, + .{ "クラウド", {} }, + .{ "通販", {} }, + .{ "网店", {} }, + .{ "संगठन", {} }, + .{ "餐厅", {} }, + .{ "网络", {} }, + .{ "ком", {} }, + .{ "亚马逊", {} }, + .{ "食品", {} }, + .{ "飞利浦", {} }, + .{ "手机", {} }, + .{ "ارامكو", {} }, + .{ "العليان", {} }, + .{ "بازار", {} }, + .{ "ابوظبي", {} }, + .{ "كاثوليك", {} }, + .{ "همراه", {} }, + .{ "닷컴", {} }, + .{ "政府", {} }, + .{ "شبكة", {} }, + .{ "بيتك", {} }, + .{ "عرب", {} }, + .{ "机构", {} }, + .{ "组织机构", {} }, + .{ "健康", {} }, + .{ "招聘", {} }, + .{ "рус", {} }, + .{ "大拿", {} }, + .{ "みんな", {} }, + .{ "グーグル", {} }, + .{ "世界", {} }, + .{ "書籍", {} }, + .{ "网址", {} }, + .{ "닷넷", {} }, + .{ "コム", {} }, + .{ "天主教", {} }, + .{ "游戏", {} }, + .{ "vermögensberater", {} }, + .{ "vermögensberatung", {} }, + .{ "企业", {} }, + .{ "信息", {} }, + .{ "嘉里大酒店", {} }, + .{ "嘉里", {} }, + .{ "广东", {} }, + .{ "政务", {} }, + .{ "xyz", {} }, + .{ "yachts", {} }, + .{ "yahoo", {} }, + .{ "yamaxun", {} }, + .{ "yandex", {} }, + .{ "yodobashi", {} }, + .{ "yoga", {} }, + .{ "yokohama", {} }, + .{ "you", {} }, + .{ "youtube", {} }, + .{ "yun", {} }, + .{ "zappos", {} }, + .{ "zara", {} }, + .{ "zero", {} }, + .{ "zip", {} }, + .{ "zone", {} }, + .{ "zuerich", {} }, + .{ "co.krd", {} }, + .{ "edu.krd", {} }, + .{ "art.pl", {} }, + .{ "gliwice.pl", {} }, + .{ "krakow.pl", {} }, + .{ "poznan.pl", {} }, + .{ "wroc.pl", {} }, + .{ "zakopane.pl", {} }, + .{ "lib.de.us", {} }, + .{ "12chars.dev", {} }, + .{ "12chars.it", {} }, + .{ "12chars.pro", {} }, + .{ "cc.ua", {} }, + .{ "inf.ua", {} }, + .{ "ltd.ua", {} }, + .{ "611.to", {} }, + .{ "a2hosted.com", {} }, + .{ "cpserver.com", {} }, + .{ "*.on-acorn.io", {} }, + .{ "activetrail.biz", {} }, + .{ "adaptable.app", {} }, + .{ "myaddr.dev", {} }, + .{ "myaddr.io", {} }, + .{ "dyn.addr.tools", {} }, + .{ "myaddr.tools", {} }, + .{ "adobeaemcloud.com", {} }, + .{ "*.dev.adobeaemcloud.com", {} }, + .{ "aem.live", {} }, + .{ "hlx.live", {} }, + .{ "adobeaemcloud.net", {} }, + .{ "aem.page", {} }, + .{ "hlx.page", {} }, + .{ "hlx3.page", {} }, + .{ "adobeio-static.net", {} }, + .{ "adobeioruntime.net", {} }, + .{ "africa.com", {} }, + .{ "beep.pl", {} }, + .{ "airkitapps.com", {} }, + .{ "airkitapps-au.com", {} }, + .{ "airkitapps.eu", {} }, + .{ "aiven.app", {} }, + .{ "aivencloud.com", {} }, + .{ "akadns.net", {} }, + .{ "akamai.net", {} }, + .{ "akamai-staging.net", {} }, + .{ "akamaiedge.net", {} }, + .{ "akamaiedge-staging.net", {} }, + .{ "akamaihd.net", {} }, + .{ "akamaihd-staging.net", {} }, + .{ "akamaiorigin.net", {} }, + .{ "akamaiorigin-staging.net", {} }, + .{ "akamaized.net", {} }, + .{ "akamaized-staging.net", {} }, + .{ "edgekey.net", {} }, + .{ "edgekey-staging.net", {} }, + .{ "edgesuite.net", {} }, + .{ "edgesuite-staging.net", {} }, + .{ "barsy.ca", {} }, + .{ "*.compute.estate", {} }, + .{ "*.alces.network", {} }, + .{ "alibabacloudcs.com", {} }, + .{ "kasserver.com", {} }, + .{ "altervista.org", {} }, + .{ "alwaysdata.net", {} }, + .{ "myamaze.net", {} }, + .{ "execute-api.cn-north-1.amazonaws.com.cn", {} }, + .{ "execute-api.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "execute-api.af-south-1.amazonaws.com", {} }, + .{ "execute-api.ap-east-1.amazonaws.com", {} }, + .{ "execute-api.ap-northeast-1.amazonaws.com", {} }, + .{ "execute-api.ap-northeast-2.amazonaws.com", {} }, + .{ "execute-api.ap-northeast-3.amazonaws.com", {} }, + .{ "execute-api.ap-south-1.amazonaws.com", {} }, + .{ "execute-api.ap-south-2.amazonaws.com", {} }, + .{ "execute-api.ap-southeast-1.amazonaws.com", {} }, + .{ "execute-api.ap-southeast-2.amazonaws.com", {} }, + .{ "execute-api.ap-southeast-3.amazonaws.com", {} }, + .{ "execute-api.ap-southeast-4.amazonaws.com", {} }, + .{ "execute-api.ap-southeast-5.amazonaws.com", {} }, + .{ "execute-api.ca-central-1.amazonaws.com", {} }, + .{ "execute-api.ca-west-1.amazonaws.com", {} }, + .{ "execute-api.eu-central-1.amazonaws.com", {} }, + .{ "execute-api.eu-central-2.amazonaws.com", {} }, + .{ "execute-api.eu-north-1.amazonaws.com", {} }, + .{ "execute-api.eu-south-1.amazonaws.com", {} }, + .{ "execute-api.eu-south-2.amazonaws.com", {} }, + .{ "execute-api.eu-west-1.amazonaws.com", {} }, + .{ "execute-api.eu-west-2.amazonaws.com", {} }, + .{ "execute-api.eu-west-3.amazonaws.com", {} }, + .{ "execute-api.il-central-1.amazonaws.com", {} }, + .{ "execute-api.me-central-1.amazonaws.com", {} }, + .{ "execute-api.me-south-1.amazonaws.com", {} }, + .{ "execute-api.sa-east-1.amazonaws.com", {} }, + .{ "execute-api.us-east-1.amazonaws.com", {} }, + .{ "execute-api.us-east-2.amazonaws.com", {} }, + .{ "execute-api.us-gov-east-1.amazonaws.com", {} }, + .{ "execute-api.us-gov-west-1.amazonaws.com", {} }, + .{ "execute-api.us-west-1.amazonaws.com", {} }, + .{ "execute-api.us-west-2.amazonaws.com", {} }, + .{ "cloudfront.net", {} }, + .{ "auth.af-south-1.amazoncognito.com", {} }, + .{ "auth.ap-east-1.amazoncognito.com", {} }, + .{ "auth.ap-northeast-1.amazoncognito.com", {} }, + .{ "auth.ap-northeast-2.amazoncognito.com", {} }, + .{ "auth.ap-northeast-3.amazoncognito.com", {} }, + .{ "auth.ap-south-1.amazoncognito.com", {} }, + .{ "auth.ap-south-2.amazoncognito.com", {} }, + .{ "auth.ap-southeast-1.amazoncognito.com", {} }, + .{ "auth.ap-southeast-2.amazoncognito.com", {} }, + .{ "auth.ap-southeast-3.amazoncognito.com", {} }, + .{ "auth.ap-southeast-4.amazoncognito.com", {} }, + .{ "auth.ca-central-1.amazoncognito.com", {} }, + .{ "auth.ca-west-1.amazoncognito.com", {} }, + .{ "auth.eu-central-1.amazoncognito.com", {} }, + .{ "auth.eu-central-2.amazoncognito.com", {} }, + .{ "auth.eu-north-1.amazoncognito.com", {} }, + .{ "auth.eu-south-1.amazoncognito.com", {} }, + .{ "auth.eu-south-2.amazoncognito.com", {} }, + .{ "auth.eu-west-1.amazoncognito.com", {} }, + .{ "auth.eu-west-2.amazoncognito.com", {} }, + .{ "auth.eu-west-3.amazoncognito.com", {} }, + .{ "auth.il-central-1.amazoncognito.com", {} }, + .{ "auth.me-central-1.amazoncognito.com", {} }, + .{ "auth.me-south-1.amazoncognito.com", {} }, + .{ "auth.sa-east-1.amazoncognito.com", {} }, + .{ "auth.us-east-1.amazoncognito.com", {} }, + .{ "auth-fips.us-east-1.amazoncognito.com", {} }, + .{ "auth.us-east-2.amazoncognito.com", {} }, + .{ "auth-fips.us-east-2.amazoncognito.com", {} }, + .{ "auth-fips.us-gov-west-1.amazoncognito.com", {} }, + .{ "auth.us-west-1.amazoncognito.com", {} }, + .{ "auth-fips.us-west-1.amazoncognito.com", {} }, + .{ "auth.us-west-2.amazoncognito.com", {} }, + .{ "auth-fips.us-west-2.amazoncognito.com", {} }, + .{ "*.compute.amazonaws.com.cn", {} }, + .{ "*.compute.amazonaws.com", {} }, + .{ "*.compute-1.amazonaws.com", {} }, + .{ "us-east-1.amazonaws.com", {} }, + .{ "emrappui-prod.cn-north-1.amazonaws.com.cn", {} }, + .{ "emrnotebooks-prod.cn-north-1.amazonaws.com.cn", {} }, + .{ "emrstudio-prod.cn-north-1.amazonaws.com.cn", {} }, + .{ "emrappui-prod.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "emrnotebooks-prod.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "emrstudio-prod.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "emrappui-prod.af-south-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.af-south-1.amazonaws.com", {} }, + .{ "emrstudio-prod.af-south-1.amazonaws.com", {} }, + .{ "emrappui-prod.ap-east-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-east-1.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-east-1.amazonaws.com", {} }, + .{ "emrappui-prod.ap-northeast-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-northeast-1.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-northeast-1.amazonaws.com", {} }, + .{ "emrappui-prod.ap-northeast-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-northeast-2.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-northeast-2.amazonaws.com", {} }, + .{ "emrappui-prod.ap-northeast-3.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-northeast-3.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-northeast-3.amazonaws.com", {} }, + .{ "emrappui-prod.ap-south-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-south-1.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-south-1.amazonaws.com", {} }, + .{ "emrappui-prod.ap-south-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-south-2.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-south-2.amazonaws.com", {} }, + .{ "emrappui-prod.ap-southeast-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-southeast-1.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-southeast-1.amazonaws.com", {} }, + .{ "emrappui-prod.ap-southeast-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-southeast-2.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-southeast-2.amazonaws.com", {} }, + .{ "emrappui-prod.ap-southeast-3.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-southeast-3.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-southeast-3.amazonaws.com", {} }, + .{ "emrappui-prod.ap-southeast-4.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ap-southeast-4.amazonaws.com", {} }, + .{ "emrstudio-prod.ap-southeast-4.amazonaws.com", {} }, + .{ "emrappui-prod.ca-central-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ca-central-1.amazonaws.com", {} }, + .{ "emrstudio-prod.ca-central-1.amazonaws.com", {} }, + .{ "emrappui-prod.ca-west-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.ca-west-1.amazonaws.com", {} }, + .{ "emrstudio-prod.ca-west-1.amazonaws.com", {} }, + .{ "emrappui-prod.eu-central-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-central-1.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-central-1.amazonaws.com", {} }, + .{ "emrappui-prod.eu-central-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-central-2.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-central-2.amazonaws.com", {} }, + .{ "emrappui-prod.eu-north-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-north-1.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-north-1.amazonaws.com", {} }, + .{ "emrappui-prod.eu-south-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-south-1.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-south-1.amazonaws.com", {} }, + .{ "emrappui-prod.eu-south-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-south-2.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-south-2.amazonaws.com", {} }, + .{ "emrappui-prod.eu-west-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-west-1.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-west-1.amazonaws.com", {} }, + .{ "emrappui-prod.eu-west-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-west-2.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-west-2.amazonaws.com", {} }, + .{ "emrappui-prod.eu-west-3.amazonaws.com", {} }, + .{ "emrnotebooks-prod.eu-west-3.amazonaws.com", {} }, + .{ "emrstudio-prod.eu-west-3.amazonaws.com", {} }, + .{ "emrappui-prod.il-central-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.il-central-1.amazonaws.com", {} }, + .{ "emrstudio-prod.il-central-1.amazonaws.com", {} }, + .{ "emrappui-prod.me-central-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.me-central-1.amazonaws.com", {} }, + .{ "emrstudio-prod.me-central-1.amazonaws.com", {} }, + .{ "emrappui-prod.me-south-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.me-south-1.amazonaws.com", {} }, + .{ "emrstudio-prod.me-south-1.amazonaws.com", {} }, + .{ "emrappui-prod.sa-east-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.sa-east-1.amazonaws.com", {} }, + .{ "emrstudio-prod.sa-east-1.amazonaws.com", {} }, + .{ "emrappui-prod.us-east-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.us-east-1.amazonaws.com", {} }, + .{ "emrstudio-prod.us-east-1.amazonaws.com", {} }, + .{ "emrappui-prod.us-east-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.us-east-2.amazonaws.com", {} }, + .{ "emrstudio-prod.us-east-2.amazonaws.com", {} }, + .{ "emrappui-prod.us-gov-east-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.us-gov-east-1.amazonaws.com", {} }, + .{ "emrstudio-prod.us-gov-east-1.amazonaws.com", {} }, + .{ "emrappui-prod.us-gov-west-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.us-gov-west-1.amazonaws.com", {} }, + .{ "emrstudio-prod.us-gov-west-1.amazonaws.com", {} }, + .{ "emrappui-prod.us-west-1.amazonaws.com", {} }, + .{ "emrnotebooks-prod.us-west-1.amazonaws.com", {} }, + .{ "emrstudio-prod.us-west-1.amazonaws.com", {} }, + .{ "emrappui-prod.us-west-2.amazonaws.com", {} }, + .{ "emrnotebooks-prod.us-west-2.amazonaws.com", {} }, + .{ "emrstudio-prod.us-west-2.amazonaws.com", {} }, + .{ "*.cn-north-1.airflow.amazonaws.com.cn", {} }, + .{ "*.cn-northwest-1.airflow.amazonaws.com.cn", {} }, + .{ "*.af-south-1.airflow.amazonaws.com", {} }, + .{ "*.ap-east-1.airflow.amazonaws.com", {} }, + .{ "*.ap-northeast-1.airflow.amazonaws.com", {} }, + .{ "*.ap-northeast-2.airflow.amazonaws.com", {} }, + .{ "*.ap-northeast-3.airflow.amazonaws.com", {} }, + .{ "*.ap-south-1.airflow.amazonaws.com", {} }, + .{ "*.ap-south-2.airflow.amazonaws.com", {} }, + .{ "*.ap-southeast-1.airflow.amazonaws.com", {} }, + .{ "*.ap-southeast-2.airflow.amazonaws.com", {} }, + .{ "*.ap-southeast-3.airflow.amazonaws.com", {} }, + .{ "*.ap-southeast-4.airflow.amazonaws.com", {} }, + .{ "*.ca-central-1.airflow.amazonaws.com", {} }, + .{ "*.ca-west-1.airflow.amazonaws.com", {} }, + .{ "*.eu-central-1.airflow.amazonaws.com", {} }, + .{ "*.eu-central-2.airflow.amazonaws.com", {} }, + .{ "*.eu-north-1.airflow.amazonaws.com", {} }, + .{ "*.eu-south-1.airflow.amazonaws.com", {} }, + .{ "*.eu-south-2.airflow.amazonaws.com", {} }, + .{ "*.eu-west-1.airflow.amazonaws.com", {} }, + .{ "*.eu-west-2.airflow.amazonaws.com", {} }, + .{ "*.eu-west-3.airflow.amazonaws.com", {} }, + .{ "*.il-central-1.airflow.amazonaws.com", {} }, + .{ "*.me-central-1.airflow.amazonaws.com", {} }, + .{ "*.me-south-1.airflow.amazonaws.com", {} }, + .{ "*.sa-east-1.airflow.amazonaws.com", {} }, + .{ "*.us-east-1.airflow.amazonaws.com", {} }, + .{ "*.us-east-2.airflow.amazonaws.com", {} }, + .{ "*.us-west-1.airflow.amazonaws.com", {} }, + .{ "*.us-west-2.airflow.amazonaws.com", {} }, + .{ "s3.dualstack.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3-accesspoint.dualstack.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3-website.dualstack.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3-accesspoint.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3-deprecated.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3-object-lambda.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3-website.cn-north-1.amazonaws.com.cn", {} }, + .{ "s3.dualstack.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "s3-accesspoint.dualstack.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "s3.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "s3-accesspoint.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "s3-object-lambda.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "s3-website.cn-northwest-1.amazonaws.com.cn", {} }, + .{ "s3.dualstack.af-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.af-south-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.af-south-1.amazonaws.com", {} }, + .{ "s3.af-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.af-south-1.amazonaws.com", {} }, + .{ "s3-object-lambda.af-south-1.amazonaws.com", {} }, + .{ "s3-website.af-south-1.amazonaws.com", {} }, + .{ "s3.dualstack.ap-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-east-1.amazonaws.com", {} }, + .{ "s3.ap-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-east-1.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-east-1.amazonaws.com", {} }, + .{ "s3-website.ap-east-1.amazonaws.com", {} }, + .{ "s3.dualstack.ap-northeast-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-northeast-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-northeast-1.amazonaws.com", {} }, + .{ "s3.ap-northeast-1.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-northeast-1.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-northeast-1.amazonaws.com", {} }, + .{ "s3-website.ap-northeast-1.amazonaws.com", {} }, + .{ "s3.dualstack.ap-northeast-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-northeast-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-northeast-2.amazonaws.com", {} }, + .{ "s3.ap-northeast-2.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-northeast-2.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-northeast-2.amazonaws.com", {} }, + .{ "s3-website.ap-northeast-2.amazonaws.com", {} }, + .{ "s3.dualstack.ap-northeast-3.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-northeast-3.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-northeast-3.amazonaws.com", {} }, + .{ "s3.ap-northeast-3.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-northeast-3.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-northeast-3.amazonaws.com", {} }, + .{ "s3-website.ap-northeast-3.amazonaws.com", {} }, + .{ "s3.dualstack.ap-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-south-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-south-1.amazonaws.com", {} }, + .{ "s3.ap-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-south-1.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-south-1.amazonaws.com", {} }, + .{ "s3-website.ap-south-1.amazonaws.com", {} }, + .{ "s3.dualstack.ap-south-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-south-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-south-2.amazonaws.com", {} }, + .{ "s3.ap-south-2.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-south-2.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-south-2.amazonaws.com", {} }, + .{ "s3-website.ap-south-2.amazonaws.com", {} }, + .{ "s3.dualstack.ap-southeast-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-southeast-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-southeast-1.amazonaws.com", {} }, + .{ "s3.ap-southeast-1.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-southeast-1.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-southeast-1.amazonaws.com", {} }, + .{ "s3-website.ap-southeast-1.amazonaws.com", {} }, + .{ "s3.dualstack.ap-southeast-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-southeast-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-southeast-2.amazonaws.com", {} }, + .{ "s3.ap-southeast-2.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-southeast-2.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-southeast-2.amazonaws.com", {} }, + .{ "s3-website.ap-southeast-2.amazonaws.com", {} }, + .{ "s3.dualstack.ap-southeast-3.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-southeast-3.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-southeast-3.amazonaws.com", {} }, + .{ "s3.ap-southeast-3.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-southeast-3.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-southeast-3.amazonaws.com", {} }, + .{ "s3-website.ap-southeast-3.amazonaws.com", {} }, + .{ "s3.dualstack.ap-southeast-4.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-southeast-4.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-southeast-4.amazonaws.com", {} }, + .{ "s3.ap-southeast-4.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-southeast-4.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-southeast-4.amazonaws.com", {} }, + .{ "s3-website.ap-southeast-4.amazonaws.com", {} }, + .{ "s3.dualstack.ap-southeast-5.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ap-southeast-5.amazonaws.com", {} }, + .{ "s3-website.dualstack.ap-southeast-5.amazonaws.com", {} }, + .{ "s3.ap-southeast-5.amazonaws.com", {} }, + .{ "s3-accesspoint.ap-southeast-5.amazonaws.com", {} }, + .{ "s3-deprecated.ap-southeast-5.amazonaws.com", {} }, + .{ "s3-object-lambda.ap-southeast-5.amazonaws.com", {} }, + .{ "s3-website.ap-southeast-5.amazonaws.com", {} }, + .{ "s3.dualstack.ca-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ca-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.ca-central-1.amazonaws.com", {} }, + .{ "s3-fips.dualstack.ca-central-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.ca-central-1.amazonaws.com", {} }, + .{ "s3.ca-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.ca-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.ca-central-1.amazonaws.com", {} }, + .{ "s3-fips.ca-central-1.amazonaws.com", {} }, + .{ "s3-object-lambda.ca-central-1.amazonaws.com", {} }, + .{ "s3-website.ca-central-1.amazonaws.com", {} }, + .{ "s3.dualstack.ca-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.ca-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.ca-west-1.amazonaws.com", {} }, + .{ "s3-fips.dualstack.ca-west-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.ca-west-1.amazonaws.com", {} }, + .{ "s3.ca-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.ca-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.ca-west-1.amazonaws.com", {} }, + .{ "s3-fips.ca-west-1.amazonaws.com", {} }, + .{ "s3-object-lambda.ca-west-1.amazonaws.com", {} }, + .{ "s3-website.ca-west-1.amazonaws.com", {} }, + .{ "s3.dualstack.eu-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-central-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.eu-central-1.amazonaws.com", {} }, + .{ "s3.eu-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-central-1.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-central-1.amazonaws.com", {} }, + .{ "s3-website.eu-central-1.amazonaws.com", {} }, + .{ "s3.dualstack.eu-central-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-central-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.eu-central-2.amazonaws.com", {} }, + .{ "s3.eu-central-2.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-central-2.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-central-2.amazonaws.com", {} }, + .{ "s3-website.eu-central-2.amazonaws.com", {} }, + .{ "s3.dualstack.eu-north-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-north-1.amazonaws.com", {} }, + .{ "s3.eu-north-1.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-north-1.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-north-1.amazonaws.com", {} }, + .{ "s3-website.eu-north-1.amazonaws.com", {} }, + .{ "s3.dualstack.eu-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-south-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.eu-south-1.amazonaws.com", {} }, + .{ "s3.eu-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-south-1.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-south-1.amazonaws.com", {} }, + .{ "s3-website.eu-south-1.amazonaws.com", {} }, + .{ "s3.dualstack.eu-south-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-south-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.eu-south-2.amazonaws.com", {} }, + .{ "s3.eu-south-2.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-south-2.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-south-2.amazonaws.com", {} }, + .{ "s3-website.eu-south-2.amazonaws.com", {} }, + .{ "s3.dualstack.eu-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-west-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.eu-west-1.amazonaws.com", {} }, + .{ "s3.eu-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-west-1.amazonaws.com", {} }, + .{ "s3-deprecated.eu-west-1.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-west-1.amazonaws.com", {} }, + .{ "s3-website.eu-west-1.amazonaws.com", {} }, + .{ "s3.dualstack.eu-west-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-west-2.amazonaws.com", {} }, + .{ "s3.eu-west-2.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-west-2.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-west-2.amazonaws.com", {} }, + .{ "s3-website.eu-west-2.amazonaws.com", {} }, + .{ "s3.dualstack.eu-west-3.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.eu-west-3.amazonaws.com", {} }, + .{ "s3-website.dualstack.eu-west-3.amazonaws.com", {} }, + .{ "s3.eu-west-3.amazonaws.com", {} }, + .{ "s3-accesspoint.eu-west-3.amazonaws.com", {} }, + .{ "s3-object-lambda.eu-west-3.amazonaws.com", {} }, + .{ "s3-website.eu-west-3.amazonaws.com", {} }, + .{ "s3.dualstack.il-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.il-central-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.il-central-1.amazonaws.com", {} }, + .{ "s3.il-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.il-central-1.amazonaws.com", {} }, + .{ "s3-object-lambda.il-central-1.amazonaws.com", {} }, + .{ "s3-website.il-central-1.amazonaws.com", {} }, + .{ "s3.dualstack.me-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.me-central-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.me-central-1.amazonaws.com", {} }, + .{ "s3.me-central-1.amazonaws.com", {} }, + .{ "s3-accesspoint.me-central-1.amazonaws.com", {} }, + .{ "s3-object-lambda.me-central-1.amazonaws.com", {} }, + .{ "s3-website.me-central-1.amazonaws.com", {} }, + .{ "s3.dualstack.me-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.me-south-1.amazonaws.com", {} }, + .{ "s3.me-south-1.amazonaws.com", {} }, + .{ "s3-accesspoint.me-south-1.amazonaws.com", {} }, + .{ "s3-object-lambda.me-south-1.amazonaws.com", {} }, + .{ "s3-website.me-south-1.amazonaws.com", {} }, + .{ "s3.amazonaws.com", {} }, + .{ "s3-1.amazonaws.com", {} }, + .{ "s3-ap-east-1.amazonaws.com", {} }, + .{ "s3-ap-northeast-1.amazonaws.com", {} }, + .{ "s3-ap-northeast-2.amazonaws.com", {} }, + .{ "s3-ap-northeast-3.amazonaws.com", {} }, + .{ "s3-ap-south-1.amazonaws.com", {} }, + .{ "s3-ap-southeast-1.amazonaws.com", {} }, + .{ "s3-ap-southeast-2.amazonaws.com", {} }, + .{ "s3-ca-central-1.amazonaws.com", {} }, + .{ "s3-eu-central-1.amazonaws.com", {} }, + .{ "s3-eu-north-1.amazonaws.com", {} }, + .{ "s3-eu-west-1.amazonaws.com", {} }, + .{ "s3-eu-west-2.amazonaws.com", {} }, + .{ "s3-eu-west-3.amazonaws.com", {} }, + .{ "s3-external-1.amazonaws.com", {} }, + .{ "s3-fips-us-gov-east-1.amazonaws.com", {} }, + .{ "s3-fips-us-gov-west-1.amazonaws.com", {} }, + .{ "mrap.accesspoint.s3-global.amazonaws.com", {} }, + .{ "s3-me-south-1.amazonaws.com", {} }, + .{ "s3-sa-east-1.amazonaws.com", {} }, + .{ "s3-us-east-2.amazonaws.com", {} }, + .{ "s3-us-gov-east-1.amazonaws.com", {} }, + .{ "s3-us-gov-west-1.amazonaws.com", {} }, + .{ "s3-us-west-1.amazonaws.com", {} }, + .{ "s3-us-west-2.amazonaws.com", {} }, + .{ "s3-website-ap-northeast-1.amazonaws.com", {} }, + .{ "s3-website-ap-southeast-1.amazonaws.com", {} }, + .{ "s3-website-ap-southeast-2.amazonaws.com", {} }, + .{ "s3-website-eu-west-1.amazonaws.com", {} }, + .{ "s3-website-sa-east-1.amazonaws.com", {} }, + .{ "s3-website-us-east-1.amazonaws.com", {} }, + .{ "s3-website-us-gov-west-1.amazonaws.com", {} }, + .{ "s3-website-us-west-1.amazonaws.com", {} }, + .{ "s3-website-us-west-2.amazonaws.com", {} }, + .{ "s3.dualstack.sa-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.sa-east-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.sa-east-1.amazonaws.com", {} }, + .{ "s3.sa-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.sa-east-1.amazonaws.com", {} }, + .{ "s3-object-lambda.sa-east-1.amazonaws.com", {} }, + .{ "s3-website.sa-east-1.amazonaws.com", {} }, + .{ "s3.dualstack.us-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.us-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.us-east-1.amazonaws.com", {} }, + .{ "s3-fips.dualstack.us-east-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.us-east-1.amazonaws.com", {} }, + .{ "s3.us-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.us-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.us-east-1.amazonaws.com", {} }, + .{ "s3-deprecated.us-east-1.amazonaws.com", {} }, + .{ "s3-fips.us-east-1.amazonaws.com", {} }, + .{ "s3-object-lambda.us-east-1.amazonaws.com", {} }, + .{ "s3-website.us-east-1.amazonaws.com", {} }, + .{ "s3.dualstack.us-east-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.us-east-2.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.us-east-2.amazonaws.com", {} }, + .{ "s3-fips.dualstack.us-east-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.us-east-2.amazonaws.com", {} }, + .{ "s3.us-east-2.amazonaws.com", {} }, + .{ "s3-accesspoint.us-east-2.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.us-east-2.amazonaws.com", {} }, + .{ "s3-deprecated.us-east-2.amazonaws.com", {} }, + .{ "s3-fips.us-east-2.amazonaws.com", {} }, + .{ "s3-object-lambda.us-east-2.amazonaws.com", {} }, + .{ "s3-website.us-east-2.amazonaws.com", {} }, + .{ "s3.dualstack.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-fips.dualstack.us-gov-east-1.amazonaws.com", {} }, + .{ "s3.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-fips.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-object-lambda.us-gov-east-1.amazonaws.com", {} }, + .{ "s3-website.us-gov-east-1.amazonaws.com", {} }, + .{ "s3.dualstack.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-fips.dualstack.us-gov-west-1.amazonaws.com", {} }, + .{ "s3.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-fips.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-object-lambda.us-gov-west-1.amazonaws.com", {} }, + .{ "s3-website.us-gov-west-1.amazonaws.com", {} }, + .{ "s3.dualstack.us-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.us-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.us-west-1.amazonaws.com", {} }, + .{ "s3-fips.dualstack.us-west-1.amazonaws.com", {} }, + .{ "s3-website.dualstack.us-west-1.amazonaws.com", {} }, + .{ "s3.us-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint.us-west-1.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.us-west-1.amazonaws.com", {} }, + .{ "s3-fips.us-west-1.amazonaws.com", {} }, + .{ "s3-object-lambda.us-west-1.amazonaws.com", {} }, + .{ "s3-website.us-west-1.amazonaws.com", {} }, + .{ "s3.dualstack.us-west-2.amazonaws.com", {} }, + .{ "s3-accesspoint.dualstack.us-west-2.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.dualstack.us-west-2.amazonaws.com", {} }, + .{ "s3-fips.dualstack.us-west-2.amazonaws.com", {} }, + .{ "s3-website.dualstack.us-west-2.amazonaws.com", {} }, + .{ "s3.us-west-2.amazonaws.com", {} }, + .{ "s3-accesspoint.us-west-2.amazonaws.com", {} }, + .{ "s3-accesspoint-fips.us-west-2.amazonaws.com", {} }, + .{ "s3-deprecated.us-west-2.amazonaws.com", {} }, + .{ "s3-fips.us-west-2.amazonaws.com", {} }, + .{ "s3-object-lambda.us-west-2.amazonaws.com", {} }, + .{ "s3-website.us-west-2.amazonaws.com", {} }, + .{ "labeling.ap-northeast-1.sagemaker.aws", {} }, + .{ "labeling.ap-northeast-2.sagemaker.aws", {} }, + .{ "labeling.ap-south-1.sagemaker.aws", {} }, + .{ "labeling.ap-southeast-1.sagemaker.aws", {} }, + .{ "labeling.ap-southeast-2.sagemaker.aws", {} }, + .{ "labeling.ca-central-1.sagemaker.aws", {} }, + .{ "labeling.eu-central-1.sagemaker.aws", {} }, + .{ "labeling.eu-west-1.sagemaker.aws", {} }, + .{ "labeling.eu-west-2.sagemaker.aws", {} }, + .{ "labeling.us-east-1.sagemaker.aws", {} }, + .{ "labeling.us-east-2.sagemaker.aws", {} }, + .{ "labeling.us-west-2.sagemaker.aws", {} }, + .{ "notebook.af-south-1.sagemaker.aws", {} }, + .{ "notebook.ap-east-1.sagemaker.aws", {} }, + .{ "notebook.ap-northeast-1.sagemaker.aws", {} }, + .{ "notebook.ap-northeast-2.sagemaker.aws", {} }, + .{ "notebook.ap-northeast-3.sagemaker.aws", {} }, + .{ "notebook.ap-south-1.sagemaker.aws", {} }, + .{ "notebook.ap-south-2.sagemaker.aws", {} }, + .{ "notebook.ap-southeast-1.sagemaker.aws", {} }, + .{ "notebook.ap-southeast-2.sagemaker.aws", {} }, + .{ "notebook.ap-southeast-3.sagemaker.aws", {} }, + .{ "notebook.ap-southeast-4.sagemaker.aws", {} }, + .{ "notebook.ca-central-1.sagemaker.aws", {} }, + .{ "notebook-fips.ca-central-1.sagemaker.aws", {} }, + .{ "notebook.ca-west-1.sagemaker.aws", {} }, + .{ "notebook-fips.ca-west-1.sagemaker.aws", {} }, + .{ "notebook.eu-central-1.sagemaker.aws", {} }, + .{ "notebook.eu-central-2.sagemaker.aws", {} }, + .{ "notebook.eu-north-1.sagemaker.aws", {} }, + .{ "notebook.eu-south-1.sagemaker.aws", {} }, + .{ "notebook.eu-south-2.sagemaker.aws", {} }, + .{ "notebook.eu-west-1.sagemaker.aws", {} }, + .{ "notebook.eu-west-2.sagemaker.aws", {} }, + .{ "notebook.eu-west-3.sagemaker.aws", {} }, + .{ "notebook.il-central-1.sagemaker.aws", {} }, + .{ "notebook.me-central-1.sagemaker.aws", {} }, + .{ "notebook.me-south-1.sagemaker.aws", {} }, + .{ "notebook.sa-east-1.sagemaker.aws", {} }, + .{ "notebook.us-east-1.sagemaker.aws", {} }, + .{ "notebook-fips.us-east-1.sagemaker.aws", {} }, + .{ "notebook.us-east-2.sagemaker.aws", {} }, + .{ "notebook-fips.us-east-2.sagemaker.aws", {} }, + .{ "notebook.us-gov-east-1.sagemaker.aws", {} }, + .{ "notebook-fips.us-gov-east-1.sagemaker.aws", {} }, + .{ "notebook.us-gov-west-1.sagemaker.aws", {} }, + .{ "notebook-fips.us-gov-west-1.sagemaker.aws", {} }, + .{ "notebook.us-west-1.sagemaker.aws", {} }, + .{ "notebook-fips.us-west-1.sagemaker.aws", {} }, + .{ "notebook.us-west-2.sagemaker.aws", {} }, + .{ "notebook-fips.us-west-2.sagemaker.aws", {} }, + .{ "notebook.cn-north-1.sagemaker.com.cn", {} }, + .{ "notebook.cn-northwest-1.sagemaker.com.cn", {} }, + .{ "studio.af-south-1.sagemaker.aws", {} }, + .{ "studio.ap-east-1.sagemaker.aws", {} }, + .{ "studio.ap-northeast-1.sagemaker.aws", {} }, + .{ "studio.ap-northeast-2.sagemaker.aws", {} }, + .{ "studio.ap-northeast-3.sagemaker.aws", {} }, + .{ "studio.ap-south-1.sagemaker.aws", {} }, + .{ "studio.ap-southeast-1.sagemaker.aws", {} }, + .{ "studio.ap-southeast-2.sagemaker.aws", {} }, + .{ "studio.ap-southeast-3.sagemaker.aws", {} }, + .{ "studio.ca-central-1.sagemaker.aws", {} }, + .{ "studio.eu-central-1.sagemaker.aws", {} }, + .{ "studio.eu-central-2.sagemaker.aws", {} }, + .{ "studio.eu-north-1.sagemaker.aws", {} }, + .{ "studio.eu-south-1.sagemaker.aws", {} }, + .{ "studio.eu-south-2.sagemaker.aws", {} }, + .{ "studio.eu-west-1.sagemaker.aws", {} }, + .{ "studio.eu-west-2.sagemaker.aws", {} }, + .{ "studio.eu-west-3.sagemaker.aws", {} }, + .{ "studio.il-central-1.sagemaker.aws", {} }, + .{ "studio.me-central-1.sagemaker.aws", {} }, + .{ "studio.me-south-1.sagemaker.aws", {} }, + .{ "studio.sa-east-1.sagemaker.aws", {} }, + .{ "studio.us-east-1.sagemaker.aws", {} }, + .{ "studio.us-east-2.sagemaker.aws", {} }, + .{ "studio.us-gov-east-1.sagemaker.aws", {} }, + .{ "studio-fips.us-gov-east-1.sagemaker.aws", {} }, + .{ "studio.us-gov-west-1.sagemaker.aws", {} }, + .{ "studio-fips.us-gov-west-1.sagemaker.aws", {} }, + .{ "studio.us-west-1.sagemaker.aws", {} }, + .{ "studio.us-west-2.sagemaker.aws", {} }, + .{ "studio.cn-north-1.sagemaker.com.cn", {} }, + .{ "studio.cn-northwest-1.sagemaker.com.cn", {} }, + .{ "*.experiments.sagemaker.aws", {} }, + .{ "analytics-gateway.ap-northeast-1.amazonaws.com", {} }, + .{ "analytics-gateway.ap-northeast-2.amazonaws.com", {} }, + .{ "analytics-gateway.ap-south-1.amazonaws.com", {} }, + .{ "analytics-gateway.ap-southeast-1.amazonaws.com", {} }, + .{ "analytics-gateway.ap-southeast-2.amazonaws.com", {} }, + .{ "analytics-gateway.eu-central-1.amazonaws.com", {} }, + .{ "analytics-gateway.eu-west-1.amazonaws.com", {} }, + .{ "analytics-gateway.us-east-1.amazonaws.com", {} }, + .{ "analytics-gateway.us-east-2.amazonaws.com", {} }, + .{ "analytics-gateway.us-west-2.amazonaws.com", {} }, + .{ "amplifyapp.com", {} }, + .{ "*.awsapprunner.com", {} }, + .{ "webview-assets.aws-cloud9.af-south-1.amazonaws.com", {} }, + .{ "vfs.cloud9.af-south-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.af-south-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-east-1.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-east-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-east-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-northeast-1.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-northeast-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-northeast-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-northeast-2.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-northeast-2.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-northeast-2.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-northeast-3.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-northeast-3.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-northeast-3.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-south-1.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-south-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-south-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-southeast-1.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-southeast-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-southeast-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ap-southeast-2.amazonaws.com", {} }, + .{ "vfs.cloud9.ap-southeast-2.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ap-southeast-2.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.ca-central-1.amazonaws.com", {} }, + .{ "vfs.cloud9.ca-central-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.ca-central-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.eu-central-1.amazonaws.com", {} }, + .{ "vfs.cloud9.eu-central-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.eu-central-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.eu-north-1.amazonaws.com", {} }, + .{ "vfs.cloud9.eu-north-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.eu-north-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.eu-south-1.amazonaws.com", {} }, + .{ "vfs.cloud9.eu-south-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.eu-south-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.eu-west-1.amazonaws.com", {} }, + .{ "vfs.cloud9.eu-west-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.eu-west-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.eu-west-2.amazonaws.com", {} }, + .{ "vfs.cloud9.eu-west-2.amazonaws.com", {} }, + .{ "webview-assets.cloud9.eu-west-2.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.eu-west-3.amazonaws.com", {} }, + .{ "vfs.cloud9.eu-west-3.amazonaws.com", {} }, + .{ "webview-assets.cloud9.eu-west-3.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.il-central-1.amazonaws.com", {} }, + .{ "vfs.cloud9.il-central-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.me-south-1.amazonaws.com", {} }, + .{ "vfs.cloud9.me-south-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.me-south-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.sa-east-1.amazonaws.com", {} }, + .{ "vfs.cloud9.sa-east-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.sa-east-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.us-east-1.amazonaws.com", {} }, + .{ "vfs.cloud9.us-east-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.us-east-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.us-east-2.amazonaws.com", {} }, + .{ "vfs.cloud9.us-east-2.amazonaws.com", {} }, + .{ "webview-assets.cloud9.us-east-2.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.us-west-1.amazonaws.com", {} }, + .{ "vfs.cloud9.us-west-1.amazonaws.com", {} }, + .{ "webview-assets.cloud9.us-west-1.amazonaws.com", {} }, + .{ "webview-assets.aws-cloud9.us-west-2.amazonaws.com", {} }, + .{ "vfs.cloud9.us-west-2.amazonaws.com", {} }, + .{ "webview-assets.cloud9.us-west-2.amazonaws.com", {} }, + .{ "awsapps.com", {} }, + .{ "cn-north-1.eb.amazonaws.com.cn", {} }, + .{ "cn-northwest-1.eb.amazonaws.com.cn", {} }, + .{ "elasticbeanstalk.com", {} }, + .{ "af-south-1.elasticbeanstalk.com", {} }, + .{ "ap-east-1.elasticbeanstalk.com", {} }, + .{ "ap-northeast-1.elasticbeanstalk.com", {} }, + .{ "ap-northeast-2.elasticbeanstalk.com", {} }, + .{ "ap-northeast-3.elasticbeanstalk.com", {} }, + .{ "ap-south-1.elasticbeanstalk.com", {} }, + .{ "ap-southeast-1.elasticbeanstalk.com", {} }, + .{ "ap-southeast-2.elasticbeanstalk.com", {} }, + .{ "ap-southeast-3.elasticbeanstalk.com", {} }, + .{ "ca-central-1.elasticbeanstalk.com", {} }, + .{ "eu-central-1.elasticbeanstalk.com", {} }, + .{ "eu-north-1.elasticbeanstalk.com", {} }, + .{ "eu-south-1.elasticbeanstalk.com", {} }, + .{ "eu-west-1.elasticbeanstalk.com", {} }, + .{ "eu-west-2.elasticbeanstalk.com", {} }, + .{ "eu-west-3.elasticbeanstalk.com", {} }, + .{ "il-central-1.elasticbeanstalk.com", {} }, + .{ "me-south-1.elasticbeanstalk.com", {} }, + .{ "sa-east-1.elasticbeanstalk.com", {} }, + .{ "us-east-1.elasticbeanstalk.com", {} }, + .{ "us-east-2.elasticbeanstalk.com", {} }, + .{ "us-gov-east-1.elasticbeanstalk.com", {} }, + .{ "us-gov-west-1.elasticbeanstalk.com", {} }, + .{ "us-west-1.elasticbeanstalk.com", {} }, + .{ "us-west-2.elasticbeanstalk.com", {} }, + .{ "*.elb.amazonaws.com.cn", {} }, + .{ "*.elb.amazonaws.com", {} }, + .{ "awsglobalaccelerator.com", {} }, + .{ "*.private.repost.aws", {} }, + .{ "transfer-webapp.ap-northeast-1.on.aws", {} }, + .{ "transfer-webapp.ap-southeast-1.on.aws", {} }, + .{ "transfer-webapp.ap-southeast-2.on.aws", {} }, + .{ "transfer-webapp.eu-central-1.on.aws", {} }, + .{ "transfer-webapp.eu-north-1.on.aws", {} }, + .{ "transfer-webapp.eu-west-1.on.aws", {} }, + .{ "transfer-webapp.us-east-1.on.aws", {} }, + .{ "transfer-webapp.us-east-2.on.aws", {} }, + .{ "transfer-webapp.us-west-2.on.aws", {} }, + .{ "eero.online", {} }, + .{ "eero-stage.online", {} }, + .{ "apigee.io", {} }, + .{ "panel.dev", {} }, + .{ "siiites.com", {} }, + .{ "appspacehosted.com", {} }, + .{ "appspaceusercontent.com", {} }, + .{ "appudo.net", {} }, + .{ "on-aptible.com", {} }, + .{ "f5.si", {} }, + .{ "arvanedge.ir", {} }, + .{ "user.aseinet.ne.jp", {} }, + .{ "gv.vc", {} }, + .{ "d.gv.vc", {} }, + .{ "user.party.eus", {} }, + .{ "pimienta.org", {} }, + .{ "poivron.org", {} }, + .{ "potager.org", {} }, + .{ "sweetpepper.org", {} }, + .{ "myasustor.com", {} }, + .{ "cdn.prod.atlassian-dev.net", {} }, + .{ "translated.page", {} }, + .{ "myfritz.link", {} }, + .{ "myfritz.net", {} }, + .{ "onavstack.net", {} }, + .{ "*.awdev.ca", {} }, + .{ "*.advisor.ws", {} }, + .{ "ecommerce-shop.pl", {} }, + .{ "b-data.io", {} }, + .{ "balena-devices.com", {} }, + .{ "base.ec", {} }, + .{ "official.ec", {} }, + .{ "buyshop.jp", {} }, + .{ "fashionstore.jp", {} }, + .{ "handcrafted.jp", {} }, + .{ "kawaiishop.jp", {} }, + .{ "supersale.jp", {} }, + .{ "theshop.jp", {} }, + .{ "shopselect.net", {} }, + .{ "base.shop", {} }, + .{ "beagleboard.io", {} }, + .{ "*.beget.app", {} }, + .{ "pages.gay", {} }, + .{ "bnr.la", {} }, + .{ "bitbucket.io", {} }, + .{ "blackbaudcdn.net", {} }, + .{ "of.je", {} }, + .{ "square.site", {} }, + .{ "bluebite.io", {} }, + .{ "boomla.net", {} }, + .{ "boutir.com", {} }, + .{ "boxfuse.io", {} }, + .{ "square7.ch", {} }, + .{ "bplaced.com", {} }, + .{ "bplaced.de", {} }, + .{ "square7.de", {} }, + .{ "bplaced.net", {} }, + .{ "square7.net", {} }, + .{ "brave.app", {} }, + .{ "*.s.brave.app", {} }, + .{ "brave.io", {} }, + .{ "*.s.brave.io", {} }, + .{ "shop.brendly.hr", {} }, + .{ "shop.brendly.rs", {} }, + .{ "browsersafetymark.io", {} }, + .{ "radio.am", {} }, + .{ "radio.fm", {} }, + .{ "cdn.bubble.io", {} }, + .{ "bubbleapps.io", {} }, + .{ "uk0.bigv.io", {} }, + .{ "dh.bytemark.co.uk", {} }, + .{ "vm.bytemark.co.uk", {} }, + .{ "cafjs.com", {} }, + .{ "canva-apps.cn", {} }, + .{ "*.my.canvasite.cn", {} }, + .{ "canva-apps.com", {} }, + .{ "*.my.canva.site", {} }, + .{ "drr.ac", {} }, + .{ "uwu.ai", {} }, + .{ "carrd.co", {} }, + .{ "crd.co", {} }, + .{ "ju.mp", {} }, + .{ "api.gov.uk", {} }, + .{ "cdn77-storage.com", {} }, + .{ "rsc.contentproxy9.cz", {} }, + .{ "r.cdn77.net", {} }, + .{ "cdn77-ssl.net", {} }, + .{ "c.cdn77.org", {} }, + .{ "rsc.cdn77.org", {} }, + .{ "ssl.origin.cdn77-secure.org", {} }, + .{ "za.bz", {} }, + .{ "br.com", {} }, + .{ "cn.com", {} }, + .{ "de.com", {} }, + .{ "eu.com", {} }, + .{ "jpn.com", {} }, + .{ "mex.com", {} }, + .{ "ru.com", {} }, + .{ "sa.com", {} }, + .{ "uk.com", {} }, + .{ "us.com", {} }, + .{ "za.com", {} }, + .{ "com.de", {} }, + .{ "gb.net", {} }, + .{ "hu.net", {} }, + .{ "jp.net", {} }, + .{ "se.net", {} }, + .{ "uk.net", {} }, + .{ "ae.org", {} }, + .{ "com.se", {} }, + .{ "cx.ua", {} }, + .{ "discourse.group", {} }, + .{ "discourse.team", {} }, + .{ "clerk.app", {} }, + .{ "clerkstage.app", {} }, + .{ "*.lcl.dev", {} }, + .{ "*.lclstage.dev", {} }, + .{ "*.stg.dev", {} }, + .{ "*.stgstage.dev", {} }, + .{ "cleverapps.cc", {} }, + .{ "*.services.clever-cloud.com", {} }, + .{ "cleverapps.io", {} }, + .{ "cleverapps.tech", {} }, + .{ "clickrising.net", {} }, + .{ "cloudns.asia", {} }, + .{ "cloudns.be", {} }, + .{ "cloud-ip.biz", {} }, + .{ "cloudns.biz", {} }, + .{ "cloudns.cc", {} }, + .{ "cloudns.ch", {} }, + .{ "cloudns.cl", {} }, + .{ "cloudns.club", {} }, + .{ "dnsabr.com", {} }, + .{ "ip-ddns.com", {} }, + .{ "cloudns.cx", {} }, + .{ "cloudns.eu", {} }, + .{ "cloudns.in", {} }, + .{ "cloudns.info", {} }, + .{ "ddns-ip.net", {} }, + .{ "dns-cloud.net", {} }, + .{ "dns-dynamic.net", {} }, + .{ "cloudns.nz", {} }, + .{ "cloudns.org", {} }, + .{ "ip-dynamic.org", {} }, + .{ "cloudns.ph", {} }, + .{ "cloudns.pro", {} }, + .{ "cloudns.pw", {} }, + .{ "cloudns.us", {} }, + .{ "c66.me", {} }, + .{ "cloud66.ws", {} }, + .{ "cloud66.zone", {} }, + .{ "jdevcloud.com", {} }, + .{ "wpdevcloud.com", {} }, + .{ "cloudaccess.host", {} }, + .{ "freesite.host", {} }, + .{ "cloudaccess.net", {} }, + .{ "cloudbeesusercontent.io", {} }, + .{ "*.cloudera.site", {} }, + .{ "cf-ipfs.com", {} }, + .{ "cloudflare-ipfs.com", {} }, + .{ "trycloudflare.com", {} }, + .{ "pages.dev", {} }, + .{ "r2.dev", {} }, + .{ "workers.dev", {} }, + .{ "cloudflare.net", {} }, + .{ "cdn.cloudflare.net", {} }, + .{ "cdn.cloudflareanycast.net", {} }, + .{ "cdn.cloudflarecn.net", {} }, + .{ "cdn.cloudflareglobal.net", {} }, + .{ "cust.cloudscale.ch", {} }, + .{ "objects.lpg.cloudscale.ch", {} }, + .{ "objects.rma.cloudscale.ch", {} }, + .{ "wnext.app", {} }, + .{ "cnpy.gdn", {} }, + .{ "*.otap.co", {} }, + .{ "co.ca", {} }, + .{ "co.com", {} }, + .{ "codeberg.page", {} }, + .{ "csb.app", {} }, + .{ "preview.csb.app", {} }, + .{ "co.nl", {} }, + .{ "co.no", {} }, + .{ "webhosting.be", {} }, + .{ "hosting-cluster.nl", {} }, + .{ "ctfcloud.net", {} }, + .{ "convex.site", {} }, + .{ "ac.ru", {} }, + .{ "edu.ru", {} }, + .{ "gov.ru", {} }, + .{ "int.ru", {} }, + .{ "mil.ru", {} }, + .{ "dyn.cosidns.de", {} }, + .{ "dnsupdater.de", {} }, + .{ "dynamisches-dns.de", {} }, + .{ "internet-dns.de", {} }, + .{ "l-o-g-i-n.de", {} }, + .{ "dynamic-dns.info", {} }, + .{ "feste-ip.net", {} }, + .{ "knx-server.net", {} }, + .{ "static-access.net", {} }, + .{ "craft.me", {} }, + .{ "realm.cz", {} }, + .{ "on.crisp.email", {} }, + .{ "*.cryptonomic.net", {} }, + .{ "cfolks.pl", {} }, + .{ "cyon.link", {} }, + .{ "cyon.site", {} }, + .{ "biz.dk", {} }, + .{ "co.dk", {} }, + .{ "firm.dk", {} }, + .{ "reg.dk", {} }, + .{ "store.dk", {} }, + .{ "dyndns.dappnode.io", {} }, + .{ "builtwithdark.com", {} }, + .{ "darklang.io", {} }, + .{ "demo.datadetect.com", {} }, + .{ "instance.datadetect.com", {} }, + .{ "edgestack.me", {} }, + .{ "dattolocal.com", {} }, + .{ "dattorelay.com", {} }, + .{ "dattoweb.com", {} }, + .{ "mydatto.com", {} }, + .{ "dattolocal.net", {} }, + .{ "mydatto.net", {} }, + .{ "ddnss.de", {} }, + .{ "dyn.ddnss.de", {} }, + .{ "dyndns.ddnss.de", {} }, + .{ "dyn-ip24.de", {} }, + .{ "dyndns1.de", {} }, + .{ "home-webserver.de", {} }, + .{ "dyn.home-webserver.de", {} }, + .{ "myhome-server.de", {} }, + .{ "ddnss.org", {} }, + .{ "debian.net", {} }, + .{ "definima.io", {} }, + .{ "definima.net", {} }, + .{ "deno.dev", {} }, + .{ "deno-staging.dev", {} }, + .{ "dedyn.io", {} }, + .{ "deta.app", {} }, + .{ "deta.dev", {} }, + .{ "dfirma.pl", {} }, + .{ "dkonto.pl", {} }, + .{ "you2.pl", {} }, + .{ "ondigitalocean.app", {} }, + .{ "*.digitaloceanspaces.com", {} }, + .{ "us.kg", {} }, + .{ "discordsays.com", {} }, + .{ "discordsez.com", {} }, + .{ "jozi.biz", {} }, + .{ "dnshome.de", {} }, + .{ "online.th", {} }, + .{ "shop.th", {} }, + .{ "drayddns.com", {} }, + .{ "shoparena.pl", {} }, + .{ "dreamhosters.com", {} }, + .{ "durumis.com", {} }, + .{ "mydrobo.com", {} }, + .{ "duckdns.org", {} }, + .{ "dy.fi", {} }, + .{ "tunk.org", {} }, + .{ "dyndns.biz", {} }, + .{ "for-better.biz", {} }, + .{ "for-more.biz", {} }, + .{ "for-some.biz", {} }, + .{ "for-the.biz", {} }, + .{ "selfip.biz", {} }, + .{ "webhop.biz", {} }, + .{ "ftpaccess.cc", {} }, + .{ "game-server.cc", {} }, + .{ "myphotos.cc", {} }, + .{ "scrapping.cc", {} }, + .{ "blogdns.com", {} }, + .{ "cechire.com", {} }, + .{ "dnsalias.com", {} }, + .{ "dnsdojo.com", {} }, + .{ "doesntexist.com", {} }, + .{ "dontexist.com", {} }, + .{ "doomdns.com", {} }, + .{ "dyn-o-saur.com", {} }, + .{ "dynalias.com", {} }, + .{ "dyndns-at-home.com", {} }, + .{ "dyndns-at-work.com", {} }, + .{ "dyndns-blog.com", {} }, + .{ "dyndns-free.com", {} }, + .{ "dyndns-home.com", {} }, + .{ "dyndns-ip.com", {} }, + .{ "dyndns-mail.com", {} }, + .{ "dyndns-office.com", {} }, + .{ "dyndns-pics.com", {} }, + .{ "dyndns-remote.com", {} }, + .{ "dyndns-server.com", {} }, + .{ "dyndns-web.com", {} }, + .{ "dyndns-wiki.com", {} }, + .{ "dyndns-work.com", {} }, + .{ "est-a-la-maison.com", {} }, + .{ "est-a-la-masion.com", {} }, + .{ "est-le-patron.com", {} }, + .{ "est-mon-blogueur.com", {} }, + .{ "from-ak.com", {} }, + .{ "from-al.com", {} }, + .{ "from-ar.com", {} }, + .{ "from-ca.com", {} }, + .{ "from-ct.com", {} }, + .{ "from-dc.com", {} }, + .{ "from-de.com", {} }, + .{ "from-fl.com", {} }, + .{ "from-ga.com", {} }, + .{ "from-hi.com", {} }, + .{ "from-ia.com", {} }, + .{ "from-id.com", {} }, + .{ "from-il.com", {} }, + .{ "from-in.com", {} }, + .{ "from-ks.com", {} }, + .{ "from-ky.com", {} }, + .{ "from-ma.com", {} }, + .{ "from-md.com", {} }, + .{ "from-mi.com", {} }, + .{ "from-mn.com", {} }, + .{ "from-mo.com", {} }, + .{ "from-ms.com", {} }, + .{ "from-mt.com", {} }, + .{ "from-nc.com", {} }, + .{ "from-nd.com", {} }, + .{ "from-ne.com", {} }, + .{ "from-nh.com", {} }, + .{ "from-nj.com", {} }, + .{ "from-nm.com", {} }, + .{ "from-nv.com", {} }, + .{ "from-oh.com", {} }, + .{ "from-ok.com", {} }, + .{ "from-or.com", {} }, + .{ "from-pa.com", {} }, + .{ "from-pr.com", {} }, + .{ "from-ri.com", {} }, + .{ "from-sc.com", {} }, + .{ "from-sd.com", {} }, + .{ "from-tn.com", {} }, + .{ "from-tx.com", {} }, + .{ "from-ut.com", {} }, + .{ "from-va.com", {} }, + .{ "from-vt.com", {} }, + .{ "from-wa.com", {} }, + .{ "from-wi.com", {} }, + .{ "from-wv.com", {} }, + .{ "from-wy.com", {} }, + .{ "getmyip.com", {} }, + .{ "gotdns.com", {} }, + .{ "hobby-site.com", {} }, + .{ "homelinux.com", {} }, + .{ "homeunix.com", {} }, + .{ "iamallama.com", {} }, + .{ "is-a-anarchist.com", {} }, + .{ "is-a-blogger.com", {} }, + .{ "is-a-bookkeeper.com", {} }, + .{ "is-a-bulls-fan.com", {} }, + .{ "is-a-caterer.com", {} }, + .{ "is-a-chef.com", {} }, + .{ "is-a-conservative.com", {} }, + .{ "is-a-cpa.com", {} }, + .{ "is-a-cubicle-slave.com", {} }, + .{ "is-a-democrat.com", {} }, + .{ "is-a-designer.com", {} }, + .{ "is-a-doctor.com", {} }, + .{ "is-a-financialadvisor.com", {} }, + .{ "is-a-geek.com", {} }, + .{ "is-a-green.com", {} }, + .{ "is-a-guru.com", {} }, + .{ "is-a-hard-worker.com", {} }, + .{ "is-a-hunter.com", {} }, + .{ "is-a-landscaper.com", {} }, + .{ "is-a-lawyer.com", {} }, + .{ "is-a-liberal.com", {} }, + .{ "is-a-libertarian.com", {} }, + .{ "is-a-llama.com", {} }, + .{ "is-a-musician.com", {} }, + .{ "is-a-nascarfan.com", {} }, + .{ "is-a-nurse.com", {} }, + .{ "is-a-painter.com", {} }, + .{ "is-a-personaltrainer.com", {} }, + .{ "is-a-photographer.com", {} }, + .{ "is-a-player.com", {} }, + .{ "is-a-republican.com", {} }, + .{ "is-a-rockstar.com", {} }, + .{ "is-a-socialist.com", {} }, + .{ "is-a-student.com", {} }, + .{ "is-a-teacher.com", {} }, + .{ "is-a-techie.com", {} }, + .{ "is-a-therapist.com", {} }, + .{ "is-an-accountant.com", {} }, + .{ "is-an-actor.com", {} }, + .{ "is-an-actress.com", {} }, + .{ "is-an-anarchist.com", {} }, + .{ "is-an-artist.com", {} }, + .{ "is-an-engineer.com", {} }, + .{ "is-an-entertainer.com", {} }, + .{ "is-certified.com", {} }, + .{ "is-gone.com", {} }, + .{ "is-into-anime.com", {} }, + .{ "is-into-cars.com", {} }, + .{ "is-into-cartoons.com", {} }, + .{ "is-into-games.com", {} }, + .{ "is-leet.com", {} }, + .{ "is-not-certified.com", {} }, + .{ "is-slick.com", {} }, + .{ "is-uberleet.com", {} }, + .{ "is-with-theband.com", {} }, + .{ "isa-geek.com", {} }, + .{ "isa-hockeynut.com", {} }, + .{ "issmarterthanyou.com", {} }, + .{ "likes-pie.com", {} }, + .{ "likescandy.com", {} }, + .{ "neat-url.com", {} }, + .{ "saves-the-whales.com", {} }, + .{ "selfip.com", {} }, + .{ "sells-for-less.com", {} }, + .{ "sells-for-u.com", {} }, + .{ "servebbs.com", {} }, + .{ "simple-url.com", {} }, + .{ "space-to-rent.com", {} }, + .{ "teaches-yoga.com", {} }, + .{ "writesthisblog.com", {} }, + .{ "ath.cx", {} }, + .{ "fuettertdasnetz.de", {} }, + .{ "isteingeek.de", {} }, + .{ "istmein.de", {} }, + .{ "lebtimnetz.de", {} }, + .{ "leitungsen.de", {} }, + .{ "traeumtgerade.de", {} }, + .{ "barrel-of-knowledge.info", {} }, + .{ "barrell-of-knowledge.info", {} }, + .{ "dyndns.info", {} }, + .{ "for-our.info", {} }, + .{ "groks-the.info", {} }, + .{ "groks-this.info", {} }, + .{ "here-for-more.info", {} }, + .{ "knowsitall.info", {} }, + .{ "selfip.info", {} }, + .{ "webhop.info", {} }, + .{ "forgot.her.name", {} }, + .{ "forgot.his.name", {} }, + .{ "at-band-camp.net", {} }, + .{ "blogdns.net", {} }, + .{ "broke-it.net", {} }, + .{ "buyshouses.net", {} }, + .{ "dnsalias.net", {} }, + .{ "dnsdojo.net", {} }, + .{ "does-it.net", {} }, + .{ "dontexist.net", {} }, + .{ "dynalias.net", {} }, + .{ "dynathome.net", {} }, + .{ "endofinternet.net", {} }, + .{ "from-az.net", {} }, + .{ "from-co.net", {} }, + .{ "from-la.net", {} }, + .{ "from-ny.net", {} }, + .{ "gets-it.net", {} }, + .{ "ham-radio-op.net", {} }, + .{ "homeftp.net", {} }, + .{ "homeip.net", {} }, + .{ "homelinux.net", {} }, + .{ "homeunix.net", {} }, + .{ "in-the-band.net", {} }, + .{ "is-a-chef.net", {} }, + .{ "is-a-geek.net", {} }, + .{ "isa-geek.net", {} }, + .{ "kicks-ass.net", {} }, + .{ "office-on-the.net", {} }, + .{ "podzone.net", {} }, + .{ "scrapper-site.net", {} }, + .{ "selfip.net", {} }, + .{ "sells-it.net", {} }, + .{ "servebbs.net", {} }, + .{ "serveftp.net", {} }, + .{ "thruhere.net", {} }, + .{ "webhop.net", {} }, + .{ "merseine.nu", {} }, + .{ "mine.nu", {} }, + .{ "shacknet.nu", {} }, + .{ "blogdns.org", {} }, + .{ "blogsite.org", {} }, + .{ "boldlygoingnowhere.org", {} }, + .{ "dnsalias.org", {} }, + .{ "dnsdojo.org", {} }, + .{ "doesntexist.org", {} }, + .{ "dontexist.org", {} }, + .{ "doomdns.org", {} }, + .{ "dvrdns.org", {} }, + .{ "dynalias.org", {} }, + .{ "dyndns.org", {} }, + .{ "go.dyndns.org", {} }, + .{ "home.dyndns.org", {} }, + .{ "endofinternet.org", {} }, + .{ "endoftheinternet.org", {} }, + .{ "from-me.org", {} }, + .{ "game-host.org", {} }, + .{ "gotdns.org", {} }, + .{ "hobby-site.org", {} }, + .{ "homedns.org", {} }, + .{ "homeftp.org", {} }, + .{ "homelinux.org", {} }, + .{ "homeunix.org", {} }, + .{ "is-a-bruinsfan.org", {} }, + .{ "is-a-candidate.org", {} }, + .{ "is-a-celticsfan.org", {} }, + .{ "is-a-chef.org", {} }, + .{ "is-a-geek.org", {} }, + .{ "is-a-knight.org", {} }, + .{ "is-a-linux-user.org", {} }, + .{ "is-a-patsfan.org", {} }, + .{ "is-a-soxfan.org", {} }, + .{ "is-found.org", {} }, + .{ "is-lost.org", {} }, + .{ "is-saved.org", {} }, + .{ "is-very-bad.org", {} }, + .{ "is-very-evil.org", {} }, + .{ "is-very-good.org", {} }, + .{ "is-very-nice.org", {} }, + .{ "is-very-sweet.org", {} }, + .{ "isa-geek.org", {} }, + .{ "kicks-ass.org", {} }, + .{ "misconfused.org", {} }, + .{ "podzone.org", {} }, + .{ "readmyblog.org", {} }, + .{ "selfip.org", {} }, + .{ "sellsyourhome.org", {} }, + .{ "servebbs.org", {} }, + .{ "serveftp.org", {} }, + .{ "servegame.org", {} }, + .{ "stuff-4-sale.org", {} }, + .{ "webhop.org", {} }, + .{ "better-than.tv", {} }, + .{ "dyndns.tv", {} }, + .{ "on-the-web.tv", {} }, + .{ "worse-than.tv", {} }, + .{ "is-by.us", {} }, + .{ "land-4-sale.us", {} }, + .{ "stuff-4-sale.us", {} }, + .{ "dyndns.ws", {} }, + .{ "mypets.ws", {} }, + .{ "ddnsfree.com", {} }, + .{ "ddnsgeek.com", {} }, + .{ "giize.com", {} }, + .{ "gleeze.com", {} }, + .{ "kozow.com", {} }, + .{ "loseyourip.com", {} }, + .{ "ooguy.com", {} }, + .{ "theworkpc.com", {} }, + .{ "casacam.net", {} }, + .{ "dynu.net", {} }, + .{ "accesscam.org", {} }, + .{ "camdvr.org", {} }, + .{ "freeddns.org", {} }, + .{ "mywire.org", {} }, + .{ "webredirect.org", {} }, + .{ "myddns.rocks", {} }, + .{ "dynv6.net", {} }, + .{ "e4.cz", {} }, + .{ "easypanel.app", {} }, + .{ "easypanel.host", {} }, + .{ "*.ewp.live", {} }, + .{ "twmail.cc", {} }, + .{ "twmail.net", {} }, + .{ "twmail.org", {} }, + .{ "mymailer.com.tw", {} }, + .{ "url.tw", {} }, + .{ "at.emf.camp", {} }, + .{ "rt.ht", {} }, + .{ "elementor.cloud", {} }, + .{ "elementor.cool", {} }, + .{ "en-root.fr", {} }, + .{ "mytuleap.com", {} }, + .{ "tuleap-partners.com", {} }, + .{ "encr.app", {} }, + .{ "encoreapi.com", {} }, + .{ "eu.encoway.cloud", {} }, + .{ "eu.org", {} }, + .{ "al.eu.org", {} }, + .{ "asso.eu.org", {} }, + .{ "at.eu.org", {} }, + .{ "au.eu.org", {} }, + .{ "be.eu.org", {} }, + .{ "bg.eu.org", {} }, + .{ "ca.eu.org", {} }, + .{ "cd.eu.org", {} }, + .{ "ch.eu.org", {} }, + .{ "cn.eu.org", {} }, + .{ "cy.eu.org", {} }, + .{ "cz.eu.org", {} }, + .{ "de.eu.org", {} }, + .{ "dk.eu.org", {} }, + .{ "edu.eu.org", {} }, + .{ "ee.eu.org", {} }, + .{ "es.eu.org", {} }, + .{ "fi.eu.org", {} }, + .{ "fr.eu.org", {} }, + .{ "gr.eu.org", {} }, + .{ "hr.eu.org", {} }, + .{ "hu.eu.org", {} }, + .{ "ie.eu.org", {} }, + .{ "il.eu.org", {} }, + .{ "in.eu.org", {} }, + .{ "int.eu.org", {} }, + .{ "is.eu.org", {} }, + .{ "it.eu.org", {} }, + .{ "jp.eu.org", {} }, + .{ "kr.eu.org", {} }, + .{ "lt.eu.org", {} }, + .{ "lu.eu.org", {} }, + .{ "lv.eu.org", {} }, + .{ "me.eu.org", {} }, + .{ "mk.eu.org", {} }, + .{ "mt.eu.org", {} }, + .{ "my.eu.org", {} }, + .{ "net.eu.org", {} }, + .{ "ng.eu.org", {} }, + .{ "nl.eu.org", {} }, + .{ "no.eu.org", {} }, + .{ "nz.eu.org", {} }, + .{ "pl.eu.org", {} }, + .{ "pt.eu.org", {} }, + .{ "ro.eu.org", {} }, + .{ "ru.eu.org", {} }, + .{ "se.eu.org", {} }, + .{ "si.eu.org", {} }, + .{ "sk.eu.org", {} }, + .{ "tr.eu.org", {} }, + .{ "uk.eu.org", {} }, + .{ "us.eu.org", {} }, + .{ "eurodir.ru", {} }, + .{ "eu-1.evennode.com", {} }, + .{ "eu-2.evennode.com", {} }, + .{ "eu-3.evennode.com", {} }, + .{ "eu-4.evennode.com", {} }, + .{ "us-1.evennode.com", {} }, + .{ "us-2.evennode.com", {} }, + .{ "us-3.evennode.com", {} }, + .{ "us-4.evennode.com", {} }, + .{ "relay.evervault.app", {} }, + .{ "relay.evervault.dev", {} }, + .{ "expo.app", {} }, + .{ "staging.expo.app", {} }, + .{ "onfabrica.com", {} }, + .{ "ru.net", {} }, + .{ "adygeya.ru", {} }, + .{ "bashkiria.ru", {} }, + .{ "bir.ru", {} }, + .{ "cbg.ru", {} }, + .{ "com.ru", {} }, + .{ "dagestan.ru", {} }, + .{ "grozny.ru", {} }, + .{ "kalmykia.ru", {} }, + .{ "kustanai.ru", {} }, + .{ "marine.ru", {} }, + .{ "mordovia.ru", {} }, + .{ "msk.ru", {} }, + .{ "mytis.ru", {} }, + .{ "nalchik.ru", {} }, + .{ "nov.ru", {} }, + .{ "pyatigorsk.ru", {} }, + .{ "spb.ru", {} }, + .{ "vladikavkaz.ru", {} }, + .{ "vladimir.ru", {} }, + .{ "abkhazia.su", {} }, + .{ "adygeya.su", {} }, + .{ "aktyubinsk.su", {} }, + .{ "arkhangelsk.su", {} }, + .{ "armenia.su", {} }, + .{ "ashgabad.su", {} }, + .{ "azerbaijan.su", {} }, + .{ "balashov.su", {} }, + .{ "bashkiria.su", {} }, + .{ "bryansk.su", {} }, + .{ "bukhara.su", {} }, + .{ "chimkent.su", {} }, + .{ "dagestan.su", {} }, + .{ "east-kazakhstan.su", {} }, + .{ "exnet.su", {} }, + .{ "georgia.su", {} }, + .{ "grozny.su", {} }, + .{ "ivanovo.su", {} }, + .{ "jambyl.su", {} }, + .{ "kalmykia.su", {} }, + .{ "kaluga.su", {} }, + .{ "karacol.su", {} }, + .{ "karaganda.su", {} }, + .{ "karelia.su", {} }, + .{ "khakassia.su", {} }, + .{ "krasnodar.su", {} }, + .{ "kurgan.su", {} }, + .{ "kustanai.su", {} }, + .{ "lenug.su", {} }, + .{ "mangyshlak.su", {} }, + .{ "mordovia.su", {} }, + .{ "msk.su", {} }, + .{ "murmansk.su", {} }, + .{ "nalchik.su", {} }, + .{ "navoi.su", {} }, + .{ "north-kazakhstan.su", {} }, + .{ "nov.su", {} }, + .{ "obninsk.su", {} }, + .{ "penza.su", {} }, + .{ "pokrovsk.su", {} }, + .{ "sochi.su", {} }, + .{ "spb.su", {} }, + .{ "tashkent.su", {} }, + .{ "termez.su", {} }, + .{ "togliatti.su", {} }, + .{ "troitsk.su", {} }, + .{ "tselinograd.su", {} }, + .{ "tula.su", {} }, + .{ "tuva.su", {} }, + .{ "vladikavkaz.su", {} }, + .{ "vladimir.su", {} }, + .{ "vologda.su", {} }, + .{ "channelsdvr.net", {} }, + .{ "u.channelsdvr.net", {} }, + .{ "edgecompute.app", {} }, + .{ "fastly-edge.com", {} }, + .{ "fastly-terrarium.com", {} }, + .{ "freetls.fastly.net", {} }, + .{ "map.fastly.net", {} }, + .{ "a.prod.fastly.net", {} }, + .{ "global.prod.fastly.net", {} }, + .{ "a.ssl.fastly.net", {} }, + .{ "b.ssl.fastly.net", {} }, + .{ "global.ssl.fastly.net", {} }, + .{ "fastlylb.net", {} }, + .{ "map.fastlylb.net", {} }, + .{ "*.user.fm", {} }, + .{ "fastvps-server.com", {} }, + .{ "fastvps.host", {} }, + .{ "myfast.host", {} }, + .{ "fastvps.site", {} }, + .{ "myfast.space", {} }, + .{ "conn.uk", {} }, + .{ "copro.uk", {} }, + .{ "hosp.uk", {} }, + .{ "fedorainfracloud.org", {} }, + .{ "fedorapeople.org", {} }, + .{ "cloud.fedoraproject.org", {} }, + .{ "app.os.fedoraproject.org", {} }, + .{ "app.os.stg.fedoraproject.org", {} }, + .{ "mydobiss.com", {} }, + .{ "fh-muenster.io", {} }, + .{ "filegear.me", {} }, + .{ "firebaseapp.com", {} }, + .{ "fldrv.com", {} }, + .{ "on-fleek.app", {} }, + .{ "flutterflow.app", {} }, + .{ "fly.dev", {} }, + .{ "shw.io", {} }, + .{ "edgeapp.net", {} }, + .{ "forgeblocks.com", {} }, + .{ "id.forgerock.io", {} }, + .{ "framer.ai", {} }, + .{ "framer.app", {} }, + .{ "framercanvas.com", {} }, + .{ "framer.media", {} }, + .{ "framer.photos", {} }, + .{ "framer.website", {} }, + .{ "framer.wiki", {} }, + .{ "*.0e.vc", {} }, + .{ "freebox-os.com", {} }, + .{ "freeboxos.com", {} }, + .{ "fbx-os.fr", {} }, + .{ "fbxos.fr", {} }, + .{ "freebox-os.fr", {} }, + .{ "freeboxos.fr", {} }, + .{ "freedesktop.org", {} }, + .{ "freemyip.com", {} }, + .{ "*.frusky.de", {} }, + .{ "wien.funkfeuer.at", {} }, + .{ "daemon.asia", {} }, + .{ "dix.asia", {} }, + .{ "mydns.bz", {} }, + .{ "0am.jp", {} }, + .{ "0g0.jp", {} }, + .{ "0j0.jp", {} }, + .{ "0t0.jp", {} }, + .{ "mydns.jp", {} }, + .{ "pgw.jp", {} }, + .{ "wjg.jp", {} }, + .{ "keyword-on.net", {} }, + .{ "live-on.net", {} }, + .{ "server-on.net", {} }, + .{ "mydns.tw", {} }, + .{ "mydns.vc", {} }, + .{ "*.futurecms.at", {} }, + .{ "*.ex.futurecms.at", {} }, + .{ "*.in.futurecms.at", {} }, + .{ "futurehosting.at", {} }, + .{ "futuremailing.at", {} }, + .{ "*.ex.ortsinfo.at", {} }, + .{ "*.kunden.ortsinfo.at", {} }, + .{ "*.statics.cloud", {} }, + .{ "aliases121.com", {} }, + .{ "campaign.gov.uk", {} }, + .{ "service.gov.uk", {} }, + .{ "independent-commission.uk", {} }, + .{ "independent-inquest.uk", {} }, + .{ "independent-inquiry.uk", {} }, + .{ "independent-panel.uk", {} }, + .{ "independent-review.uk", {} }, + .{ "public-inquiry.uk", {} }, + .{ "royal-commission.uk", {} }, + .{ "gehirn.ne.jp", {} }, + .{ "usercontent.jp", {} }, + .{ "gentapps.com", {} }, + .{ "gentlentapis.com", {} }, + .{ "cdn-edges.net", {} }, + .{ "gsj.bz", {} }, + .{ "githubusercontent.com", {} }, + .{ "githubpreview.dev", {} }, + .{ "github.io", {} }, + .{ "gitlab.io", {} }, + .{ "gitapp.si", {} }, + .{ "gitpage.si", {} }, + .{ "glitch.me", {} }, + .{ "nog.community", {} }, + .{ "co.ro", {} }, + .{ "shop.ro", {} }, + .{ "lolipop.io", {} }, + .{ "angry.jp", {} }, + .{ "babyblue.jp", {} }, + .{ "babymilk.jp", {} }, + .{ "backdrop.jp", {} }, + .{ "bambina.jp", {} }, + .{ "bitter.jp", {} }, + .{ "blush.jp", {} }, + .{ "boo.jp", {} }, + .{ "boy.jp", {} }, + .{ "boyfriend.jp", {} }, + .{ "but.jp", {} }, + .{ "candypop.jp", {} }, + .{ "capoo.jp", {} }, + .{ "catfood.jp", {} }, + .{ "cheap.jp", {} }, + .{ "chicappa.jp", {} }, + .{ "chillout.jp", {} }, + .{ "chips.jp", {} }, + .{ "chowder.jp", {} }, + .{ "chu.jp", {} }, + .{ "ciao.jp", {} }, + .{ "cocotte.jp", {} }, + .{ "coolblog.jp", {} }, + .{ "cranky.jp", {} }, + .{ "cutegirl.jp", {} }, + .{ "daa.jp", {} }, + .{ "deca.jp", {} }, + .{ "deci.jp", {} }, + .{ "digick.jp", {} }, + .{ "egoism.jp", {} }, + .{ "fakefur.jp", {} }, + .{ "fem.jp", {} }, + .{ "flier.jp", {} }, + .{ "floppy.jp", {} }, + .{ "fool.jp", {} }, + .{ "frenchkiss.jp", {} }, + .{ "girlfriend.jp", {} }, + .{ "girly.jp", {} }, + .{ "gloomy.jp", {} }, + .{ "gonna.jp", {} }, + .{ "greater.jp", {} }, + .{ "hacca.jp", {} }, + .{ "heavy.jp", {} }, + .{ "her.jp", {} }, + .{ "hiho.jp", {} }, + .{ "hippy.jp", {} }, + .{ "holy.jp", {} }, + .{ "hungry.jp", {} }, + .{ "icurus.jp", {} }, + .{ "itigo.jp", {} }, + .{ "jellybean.jp", {} }, + .{ "kikirara.jp", {} }, + .{ "kill.jp", {} }, + .{ "kilo.jp", {} }, + .{ "kuron.jp", {} }, + .{ "littlestar.jp", {} }, + .{ "lolipopmc.jp", {} }, + .{ "lolitapunk.jp", {} }, + .{ "lomo.jp", {} }, + .{ "lovepop.jp", {} }, + .{ "lovesick.jp", {} }, + .{ "main.jp", {} }, + .{ "mods.jp", {} }, + .{ "mond.jp", {} }, + .{ "mongolian.jp", {} }, + .{ "moo.jp", {} }, + .{ "namaste.jp", {} }, + .{ "nikita.jp", {} }, + .{ "nobushi.jp", {} }, + .{ "noor.jp", {} }, + .{ "oops.jp", {} }, + .{ "parallel.jp", {} }, + .{ "parasite.jp", {} }, + .{ "pecori.jp", {} }, + .{ "peewee.jp", {} }, + .{ "penne.jp", {} }, + .{ "pepper.jp", {} }, + .{ "perma.jp", {} }, + .{ "pigboat.jp", {} }, + .{ "pinoko.jp", {} }, + .{ "punyu.jp", {} }, + .{ "pupu.jp", {} }, + .{ "pussycat.jp", {} }, + .{ "pya.jp", {} }, + .{ "raindrop.jp", {} }, + .{ "readymade.jp", {} }, + .{ "sadist.jp", {} }, + .{ "schoolbus.jp", {} }, + .{ "secret.jp", {} }, + .{ "staba.jp", {} }, + .{ "stripper.jp", {} }, + .{ "sub.jp", {} }, + .{ "sunnyday.jp", {} }, + .{ "thick.jp", {} }, + .{ "tonkotsu.jp", {} }, + .{ "under.jp", {} }, + .{ "upper.jp", {} }, + .{ "velvet.jp", {} }, + .{ "verse.jp", {} }, + .{ "versus.jp", {} }, + .{ "vivian.jp", {} }, + .{ "watson.jp", {} }, + .{ "weblike.jp", {} }, + .{ "whitesnow.jp", {} }, + .{ "zombie.jp", {} }, + .{ "heteml.net", {} }, + .{ "graphic.design", {} }, + .{ "goip.de", {} }, + .{ "*.hosted.app", {} }, + .{ "*.run.app", {} }, + .{ "web.app", {} }, + .{ "*.0emm.com", {} }, + .{ "appspot.com", {} }, + .{ "*.r.appspot.com", {} }, + .{ "blogspot.com", {} }, + .{ "codespot.com", {} }, + .{ "googleapis.com", {} }, + .{ "googlecode.com", {} }, + .{ "pagespeedmobilizer.com", {} }, + .{ "withgoogle.com", {} }, + .{ "withyoutube.com", {} }, + .{ "*.gateway.dev", {} }, + .{ "cloud.goog", {} }, + .{ "translate.goog", {} }, + .{ "*.usercontent.goog", {} }, + .{ "cloudfunctions.net", {} }, + .{ "goupile.fr", {} }, + .{ "pymnt.uk", {} }, + .{ "cloudapps.digital", {} }, + .{ "london.cloudapps.digital", {} }, + .{ "gov.nl", {} }, + .{ "grafana-dev.net", {} }, + .{ "grayjayleagues.com", {} }, + .{ "günstigbestellen.de", {} }, + .{ "günstigliefern.de", {} }, + .{ "häkkinen.fi", {} }, + .{ "hrsn.dev", {} }, + .{ "hashbang.sh", {} }, + .{ "hasura.app", {} }, + .{ "hasura-app.io", {} }, + .{ "hatenablog.com", {} }, + .{ "hatenadiary.com", {} }, + .{ "hateblo.jp", {} }, + .{ "hatenablog.jp", {} }, + .{ "hatenadiary.jp", {} }, + .{ "hatenadiary.org", {} }, + .{ "pages.it.hs-heilbronn.de", {} }, + .{ "pages-research.it.hs-heilbronn.de", {} }, + .{ "heiyu.space", {} }, + .{ "helioho.st", {} }, + .{ "heliohost.us", {} }, + .{ "hepforge.org", {} }, + .{ "herokuapp.com", {} }, + .{ "heyflow.page", {} }, + .{ "heyflow.site", {} }, + .{ "ravendb.cloud", {} }, + .{ "ravendb.community", {} }, + .{ "development.run", {} }, + .{ "ravendb.run", {} }, + .{ "homesklep.pl", {} }, + .{ "*.kin.one", {} }, + .{ "*.id.pub", {} }, + .{ "*.kin.pub", {} }, + .{ "hoplix.shop", {} }, + .{ "orx.biz", {} }, + .{ "biz.gl", {} }, + .{ "biz.ng", {} }, + .{ "co.biz.ng", {} }, + .{ "dl.biz.ng", {} }, + .{ "go.biz.ng", {} }, + .{ "lg.biz.ng", {} }, + .{ "on.biz.ng", {} }, + .{ "col.ng", {} }, + .{ "firm.ng", {} }, + .{ "gen.ng", {} }, + .{ "ltd.ng", {} }, + .{ "ngo.ng", {} }, + .{ "plc.ng", {} }, + .{ "ie.ua", {} }, + .{ "hostyhosting.io", {} }, + .{ "hf.space", {} }, + .{ "static.hf.space", {} }, + .{ "hypernode.io", {} }, + .{ "iobb.net", {} }, + .{ "co.cz", {} }, + .{ "*.moonscale.io", {} }, + .{ "moonscale.net", {} }, + .{ "gr.com", {} }, + .{ "iki.fi", {} }, + .{ "ibxos.it", {} }, + .{ "iliadboxos.it", {} }, + .{ "smushcdn.com", {} }, + .{ "wphostedmail.com", {} }, + .{ "wpmucdn.com", {} }, + .{ "tempurl.host", {} }, + .{ "wpmudev.host", {} }, + .{ "dyn-berlin.de", {} }, + .{ "in-berlin.de", {} }, + .{ "in-brb.de", {} }, + .{ "in-butter.de", {} }, + .{ "in-dsl.de", {} }, + .{ "in-vpn.de", {} }, + .{ "in-dsl.net", {} }, + .{ "in-vpn.net", {} }, + .{ "in-dsl.org", {} }, + .{ "in-vpn.org", {} }, + .{ "biz.at", {} }, + .{ "info.at", {} }, + .{ "info.cx", {} }, + .{ "ac.leg.br", {} }, + .{ "al.leg.br", {} }, + .{ "am.leg.br", {} }, + .{ "ap.leg.br", {} }, + .{ "ba.leg.br", {} }, + .{ "ce.leg.br", {} }, + .{ "df.leg.br", {} }, + .{ "es.leg.br", {} }, + .{ "go.leg.br", {} }, + .{ "ma.leg.br", {} }, + .{ "mg.leg.br", {} }, + .{ "ms.leg.br", {} }, + .{ "mt.leg.br", {} }, + .{ "pa.leg.br", {} }, + .{ "pb.leg.br", {} }, + .{ "pe.leg.br", {} }, + .{ "pi.leg.br", {} }, + .{ "pr.leg.br", {} }, + .{ "rj.leg.br", {} }, + .{ "rn.leg.br", {} }, + .{ "ro.leg.br", {} }, + .{ "rr.leg.br", {} }, + .{ "rs.leg.br", {} }, + .{ "sc.leg.br", {} }, + .{ "se.leg.br", {} }, + .{ "sp.leg.br", {} }, + .{ "to.leg.br", {} }, + .{ "pixolino.com", {} }, + .{ "na4u.ru", {} }, + .{ "botdash.app", {} }, + .{ "botdash.dev", {} }, + .{ "botdash.gg", {} }, + .{ "botdash.net", {} }, + .{ "botda.sh", {} }, + .{ "botdash.xyz", {} }, + .{ "apps-1and1.com", {} }, + .{ "live-website.com", {} }, + .{ "apps-1and1.net", {} }, + .{ "websitebuilder.online", {} }, + .{ "app-ionos.space", {} }, + .{ "iopsys.se", {} }, + .{ "*.dweb.link", {} }, + .{ "ipifony.net", {} }, + .{ "ir.md", {} }, + .{ "is-a-good.dev", {} }, + .{ "is-a.dev", {} }, + .{ "iservschule.de", {} }, + .{ "mein-iserv.de", {} }, + .{ "schulplattform.de", {} }, + .{ "schulserver.de", {} }, + .{ "test-iserv.de", {} }, + .{ "iserv.dev", {} }, + .{ "mel.cloudlets.com.au", {} }, + .{ "cloud.interhostsolutions.be", {} }, + .{ "alp1.ae.flow.ch", {} }, + .{ "appengine.flow.ch", {} }, + .{ "es-1.axarnet.cloud", {} }, + .{ "diadem.cloud", {} }, + .{ "vip.jelastic.cloud", {} }, + .{ "jele.cloud", {} }, + .{ "it1.eur.aruba.jenv-aruba.cloud", {} }, + .{ "it1.jenv-aruba.cloud", {} }, + .{ "keliweb.cloud", {} }, + .{ "cs.keliweb.cloud", {} }, + .{ "oxa.cloud", {} }, + .{ "tn.oxa.cloud", {} }, + .{ "uk.oxa.cloud", {} }, + .{ "primetel.cloud", {} }, + .{ "uk.primetel.cloud", {} }, + .{ "ca.reclaim.cloud", {} }, + .{ "uk.reclaim.cloud", {} }, + .{ "us.reclaim.cloud", {} }, + .{ "ch.trendhosting.cloud", {} }, + .{ "de.trendhosting.cloud", {} }, + .{ "jele.club", {} }, + .{ "dopaas.com", {} }, + .{ "paas.hosted-by-previder.com", {} }, + .{ "rag-cloud.hosteur.com", {} }, + .{ "rag-cloud-ch.hosteur.com", {} }, + .{ "jcloud.ik-server.com", {} }, + .{ "jcloud-ver-jpc.ik-server.com", {} }, + .{ "demo.jelastic.com", {} }, + .{ "paas.massivegrid.com", {} }, + .{ "jed.wafaicloud.com", {} }, + .{ "ryd.wafaicloud.com", {} }, + .{ "j.scaleforce.com.cy", {} }, + .{ "jelastic.dogado.eu", {} }, + .{ "fi.cloudplatform.fi", {} }, + .{ "demo.datacenter.fi", {} }, + .{ "paas.datacenter.fi", {} }, + .{ "jele.host", {} }, + .{ "mircloud.host", {} }, + .{ "paas.beebyte.io", {} }, + .{ "sekd1.beebyteapp.io", {} }, + .{ "jele.io", {} }, + .{ "jc.neen.it", {} }, + .{ "jcloud.kz", {} }, + .{ "cloudjiffy.net", {} }, + .{ "fra1-de.cloudjiffy.net", {} }, + .{ "west1-us.cloudjiffy.net", {} }, + .{ "jls-sto1.elastx.net", {} }, + .{ "jls-sto2.elastx.net", {} }, + .{ "jls-sto3.elastx.net", {} }, + .{ "fr-1.paas.massivegrid.net", {} }, + .{ "lon-1.paas.massivegrid.net", {} }, + .{ "lon-2.paas.massivegrid.net", {} }, + .{ "ny-1.paas.massivegrid.net", {} }, + .{ "ny-2.paas.massivegrid.net", {} }, + .{ "sg-1.paas.massivegrid.net", {} }, + .{ "jelastic.saveincloud.net", {} }, + .{ "nordeste-idc.saveincloud.net", {} }, + .{ "j.scaleforce.net", {} }, + .{ "sdscloud.pl", {} }, + .{ "unicloud.pl", {} }, + .{ "mircloud.ru", {} }, + .{ "enscaled.sg", {} }, + .{ "jele.site", {} }, + .{ "jelastic.team", {} }, + .{ "orangecloud.tn", {} }, + .{ "j.layershift.co.uk", {} }, + .{ "phx.enscaled.us", {} }, + .{ "mircloud.us", {} }, + .{ "myjino.ru", {} }, + .{ "*.hosting.myjino.ru", {} }, + .{ "*.landing.myjino.ru", {} }, + .{ "*.spectrum.myjino.ru", {} }, + .{ "*.vps.myjino.ru", {} }, + .{ "jotelulu.cloud", {} }, + .{ "webadorsite.com", {} }, + .{ "jouwweb.site", {} }, + .{ "*.cns.joyent.com", {} }, + .{ "*.triton.zone", {} }, + .{ "js.org", {} }, + .{ "kaas.gg", {} }, + .{ "khplay.nl", {} }, + .{ "kapsi.fi", {} }, + .{ "ezproxy.kuleuven.be", {} }, + .{ "kuleuven.cloud", {} }, + .{ "keymachine.de", {} }, + .{ "kinghost.net", {} }, + .{ "uni5.net", {} }, + .{ "knightpoint.systems", {} }, + .{ "koobin.events", {} }, + .{ "webthings.io", {} }, + .{ "krellian.net", {} }, + .{ "oya.to", {} }, + .{ "laravel.cloud", {} }, + .{ "git-repos.de", {} }, + .{ "lcube-server.de", {} }, + .{ "svn-repos.de", {} }, + .{ "leadpages.co", {} }, + .{ "lpages.co", {} }, + .{ "lpusercontent.com", {} }, + .{ "liara.run", {} }, + .{ "iran.liara.run", {} }, + .{ "libp2p.direct", {} }, + .{ "runcontainers.dev", {} }, + .{ "co.business", {} }, + .{ "co.education", {} }, + .{ "co.events", {} }, + .{ "co.financial", {} }, + .{ "co.network", {} }, + .{ "co.place", {} }, + .{ "co.technology", {} }, + .{ "linkyard-cloud.ch", {} }, + .{ "linkyard.cloud", {} }, + .{ "members.linode.com", {} }, + .{ "*.nodebalancer.linode.com", {} }, + .{ "*.linodeobjects.com", {} }, + .{ "ip.linodeusercontent.com", {} }, + .{ "we.bs", {} }, + .{ "filegear-sg.me", {} }, + .{ "ggff.net", {} }, + .{ "*.user.localcert.dev", {} }, + .{ "localcert.net", {} }, + .{ "localhostcert.net", {} }, + .{ "localtonet.com", {} }, + .{ "*.localto.net", {} }, + .{ "lodz.pl", {} }, + .{ "pabianice.pl", {} }, + .{ "plock.pl", {} }, + .{ "sieradz.pl", {} }, + .{ "skierniewice.pl", {} }, + .{ "zgierz.pl", {} }, + .{ "loginline.app", {} }, + .{ "loginline.dev", {} }, + .{ "loginline.io", {} }, + .{ "loginline.services", {} }, + .{ "loginline.site", {} }, + .{ "lohmus.me", {} }, + .{ "servers.run", {} }, + .{ "lovable.app", {} }, + .{ "lovableproject.com", {} }, + .{ "krasnik.pl", {} }, + .{ "leczna.pl", {} }, + .{ "lubartow.pl", {} }, + .{ "lublin.pl", {} }, + .{ "poniatowa.pl", {} }, + .{ "swidnik.pl", {} }, + .{ "glug.org.uk", {} }, + .{ "lug.org.uk", {} }, + .{ "lugs.org.uk", {} }, + .{ "barsy.bg", {} }, + .{ "barsy.club", {} }, + .{ "barsycenter.com", {} }, + .{ "barsyonline.com", {} }, + .{ "barsy.de", {} }, + .{ "barsy.dev", {} }, + .{ "barsy.eu", {} }, + .{ "barsy.gr", {} }, + .{ "barsy.in", {} }, + .{ "barsy.info", {} }, + .{ "barsy.io", {} }, + .{ "barsy.me", {} }, + .{ "barsy.menu", {} }, + .{ "barsyonline.menu", {} }, + .{ "barsy.mobi", {} }, + .{ "barsy.net", {} }, + .{ "barsy.online", {} }, + .{ "barsy.org", {} }, + .{ "barsy.pro", {} }, + .{ "barsy.pub", {} }, + .{ "barsy.ro", {} }, + .{ "barsy.rs", {} }, + .{ "barsy.shop", {} }, + .{ "barsyonline.shop", {} }, + .{ "barsy.site", {} }, + .{ "barsy.store", {} }, + .{ "barsy.support", {} }, + .{ "barsy.uk", {} }, + .{ "barsy.co.uk", {} }, + .{ "barsyonline.co.uk", {} }, + .{ "*.magentosite.cloud", {} }, + .{ "hb.cldmail.ru", {} }, + .{ "matlab.cloud", {} }, + .{ "modelscape.com", {} }, + .{ "mwcloudnonprod.com", {} }, + .{ "polyspace.com", {} }, + .{ "mayfirst.info", {} }, + .{ "mayfirst.org", {} }, + .{ "mazeplay.com", {} }, + .{ "mcdir.me", {} }, + .{ "mcdir.ru", {} }, + .{ "vps.mcdir.ru", {} }, + .{ "mcpre.ru", {} }, + .{ "mediatech.by", {} }, + .{ "mediatech.dev", {} }, + .{ "hra.health", {} }, + .{ "medusajs.app", {} }, + .{ "miniserver.com", {} }, + .{ "memset.net", {} }, + .{ "messerli.app", {} }, + .{ "atmeta.com", {} }, + .{ "apps.fbsbx.com", {} }, + .{ "*.cloud.metacentrum.cz", {} }, + .{ "custom.metacentrum.cz", {} }, + .{ "flt.cloud.muni.cz", {} }, + .{ "usr.cloud.muni.cz", {} }, + .{ "meteorapp.com", {} }, + .{ "eu.meteorapp.com", {} }, + .{ "co.pl", {} }, + .{ "*.azurecontainer.io", {} }, + .{ "azure-api.net", {} }, + .{ "azure-mobile.net", {} }, + .{ "azureedge.net", {} }, + .{ "azurefd.net", {} }, + .{ "azurestaticapps.net", {} }, + .{ "1.azurestaticapps.net", {} }, + .{ "2.azurestaticapps.net", {} }, + .{ "3.azurestaticapps.net", {} }, + .{ "4.azurestaticapps.net", {} }, + .{ "5.azurestaticapps.net", {} }, + .{ "6.azurestaticapps.net", {} }, + .{ "7.azurestaticapps.net", {} }, + .{ "centralus.azurestaticapps.net", {} }, + .{ "eastasia.azurestaticapps.net", {} }, + .{ "eastus2.azurestaticapps.net", {} }, + .{ "westeurope.azurestaticapps.net", {} }, + .{ "westus2.azurestaticapps.net", {} }, + .{ "azurewebsites.net", {} }, + .{ "cloudapp.net", {} }, + .{ "trafficmanager.net", {} }, + .{ "blob.core.windows.net", {} }, + .{ "servicebus.windows.net", {} }, + .{ "routingthecloud.com", {} }, + .{ "sn.mynetname.net", {} }, + .{ "routingthecloud.net", {} }, + .{ "routingthecloud.org", {} }, + .{ "csx.cc", {} }, + .{ "mydbserver.com", {} }, + .{ "webspaceconfig.de", {} }, + .{ "mittwald.info", {} }, + .{ "mittwaldserver.info", {} }, + .{ "typo3server.info", {} }, + .{ "project.space", {} }, + .{ "modx.dev", {} }, + .{ "bmoattachments.org", {} }, + .{ "net.ru", {} }, + .{ "org.ru", {} }, + .{ "pp.ru", {} }, + .{ "hostedpi.com", {} }, + .{ "caracal.mythic-beasts.com", {} }, + .{ "customer.mythic-beasts.com", {} }, + .{ "fentiger.mythic-beasts.com", {} }, + .{ "lynx.mythic-beasts.com", {} }, + .{ "ocelot.mythic-beasts.com", {} }, + .{ "oncilla.mythic-beasts.com", {} }, + .{ "onza.mythic-beasts.com", {} }, + .{ "sphinx.mythic-beasts.com", {} }, + .{ "vs.mythic-beasts.com", {} }, + .{ "x.mythic-beasts.com", {} }, + .{ "yali.mythic-beasts.com", {} }, + .{ "cust.retrosnub.co.uk", {} }, + .{ "ui.nabu.casa", {} }, + .{ "cloud.nospamproxy.com", {} }, + .{ "o365.cloud.nospamproxy.com", {} }, + .{ "netlib.re", {} }, + .{ "netfy.app", {} }, + .{ "netlify.app", {} }, + .{ "4u.com", {} }, + .{ "nfshost.com", {} }, + .{ "ipfs.nftstorage.link", {} }, + .{ "ngo.us", {} }, + .{ "ngrok.app", {} }, + .{ "ngrok-free.app", {} }, + .{ "ngrok.dev", {} }, + .{ "ngrok-free.dev", {} }, + .{ "ngrok.io", {} }, + .{ "ap.ngrok.io", {} }, + .{ "au.ngrok.io", {} }, + .{ "eu.ngrok.io", {} }, + .{ "in.ngrok.io", {} }, + .{ "jp.ngrok.io", {} }, + .{ "sa.ngrok.io", {} }, + .{ "us.ngrok.io", {} }, + .{ "ngrok.pizza", {} }, + .{ "ngrok.pro", {} }, + .{ "torun.pl", {} }, + .{ "nh-serv.co.uk", {} }, + .{ "nimsite.uk", {} }, + .{ "mmafan.biz", {} }, + .{ "myftp.biz", {} }, + .{ "no-ip.biz", {} }, + .{ "no-ip.ca", {} }, + .{ "fantasyleague.cc", {} }, + .{ "gotdns.ch", {} }, + .{ "3utilities.com", {} }, + .{ "blogsyte.com", {} }, + .{ "ciscofreak.com", {} }, + .{ "damnserver.com", {} }, + .{ "ddnsking.com", {} }, + .{ "ditchyourip.com", {} }, + .{ "dnsiskinky.com", {} }, + .{ "dynns.com", {} }, + .{ "geekgalaxy.com", {} }, + .{ "health-carereform.com", {} }, + .{ "homesecuritymac.com", {} }, + .{ "homesecuritypc.com", {} }, + .{ "myactivedirectory.com", {} }, + .{ "mysecuritycamera.com", {} }, + .{ "myvnc.com", {} }, + .{ "net-freaks.com", {} }, + .{ "onthewifi.com", {} }, + .{ "point2this.com", {} }, + .{ "quicksytes.com", {} }, + .{ "securitytactics.com", {} }, + .{ "servebeer.com", {} }, + .{ "servecounterstrike.com", {} }, + .{ "serveexchange.com", {} }, + .{ "serveftp.com", {} }, + .{ "servegame.com", {} }, + .{ "servehalflife.com", {} }, + .{ "servehttp.com", {} }, + .{ "servehumour.com", {} }, + .{ "serveirc.com", {} }, + .{ "servemp3.com", {} }, + .{ "servep2p.com", {} }, + .{ "servepics.com", {} }, + .{ "servequake.com", {} }, + .{ "servesarcasm.com", {} }, + .{ "stufftoread.com", {} }, + .{ "unusualperson.com", {} }, + .{ "workisboring.com", {} }, + .{ "dvrcam.info", {} }, + .{ "ilovecollege.info", {} }, + .{ "no-ip.info", {} }, + .{ "brasilia.me", {} }, + .{ "ddns.me", {} }, + .{ "dnsfor.me", {} }, + .{ "hopto.me", {} }, + .{ "loginto.me", {} }, + .{ "noip.me", {} }, + .{ "webhop.me", {} }, + .{ "bounceme.net", {} }, + .{ "ddns.net", {} }, + .{ "eating-organic.net", {} }, + .{ "mydissent.net", {} }, + .{ "myeffect.net", {} }, + .{ "mymediapc.net", {} }, + .{ "mypsx.net", {} }, + .{ "mysecuritycamera.net", {} }, + .{ "nhlfan.net", {} }, + .{ "no-ip.net", {} }, + .{ "pgafan.net", {} }, + .{ "privatizehealthinsurance.net", {} }, + .{ "redirectme.net", {} }, + .{ "serveblog.net", {} }, + .{ "serveminecraft.net", {} }, + .{ "sytes.net", {} }, + .{ "cable-modem.org", {} }, + .{ "collegefan.org", {} }, + .{ "couchpotatofries.org", {} }, + .{ "hopto.org", {} }, + .{ "mlbfan.org", {} }, + .{ "myftp.org", {} }, + .{ "mysecuritycamera.org", {} }, + .{ "nflfan.org", {} }, + .{ "no-ip.org", {} }, + .{ "read-books.org", {} }, + .{ "ufcfan.org", {} }, + .{ "zapto.org", {} }, + .{ "no-ip.co.uk", {} }, + .{ "golffan.us", {} }, + .{ "noip.us", {} }, + .{ "pointto.us", {} }, + .{ "stage.nodeart.io", {} }, + .{ "*.developer.app", {} }, + .{ "noop.app", {} }, + .{ "*.northflank.app", {} }, + .{ "*.build.run", {} }, + .{ "*.code.run", {} }, + .{ "*.database.run", {} }, + .{ "*.migration.run", {} }, + .{ "noticeable.news", {} }, + .{ "notion.site", {} }, + .{ "dnsking.ch", {} }, + .{ "mypi.co", {} }, + .{ "myiphost.com", {} }, + .{ "forumz.info", {} }, + .{ "soundcast.me", {} }, + .{ "tcp4.me", {} }, + .{ "dnsup.net", {} }, + .{ "hicam.net", {} }, + .{ "now-dns.net", {} }, + .{ "ownip.net", {} }, + .{ "vpndns.net", {} }, + .{ "dynserv.org", {} }, + .{ "now-dns.org", {} }, + .{ "x443.pw", {} }, + .{ "ntdll.top", {} }, + .{ "freeddns.us", {} }, + .{ "nsupdate.info", {} }, + .{ "nerdpol.ovh", {} }, + .{ "nyc.mn", {} }, + .{ "prvcy.page", {} }, + .{ "obl.ong", {} }, + .{ "observablehq.cloud", {} }, + .{ "static.observableusercontent.com", {} }, + .{ "omg.lol", {} }, + .{ "cloudycluster.net", {} }, + .{ "omniwe.site", {} }, + .{ "123webseite.at", {} }, + .{ "123website.be", {} }, + .{ "simplesite.com.br", {} }, + .{ "123website.ch", {} }, + .{ "simplesite.com", {} }, + .{ "123webseite.de", {} }, + .{ "123hjemmeside.dk", {} }, + .{ "123miweb.es", {} }, + .{ "123kotisivu.fi", {} }, + .{ "123siteweb.fr", {} }, + .{ "simplesite.gr", {} }, + .{ "123homepage.it", {} }, + .{ "123website.lu", {} }, + .{ "123website.nl", {} }, + .{ "123hjemmeside.no", {} }, + .{ "service.one", {} }, + .{ "simplesite.pl", {} }, + .{ "123paginaweb.pt", {} }, + .{ "123minsida.se", {} }, + .{ "is-a-fullstack.dev", {} }, + .{ "is-cool.dev", {} }, + .{ "is-not-a.dev", {} }, + .{ "localplayer.dev", {} }, + .{ "is-local.org", {} }, + .{ "opensocial.site", {} }, + .{ "opencraft.hosting", {} }, + .{ "16-b.it", {} }, + .{ "32-b.it", {} }, + .{ "64-b.it", {} }, + .{ "orsites.com", {} }, + .{ "operaunite.com", {} }, + .{ "*.customer-oci.com", {} }, + .{ "*.oci.customer-oci.com", {} }, + .{ "*.ocp.customer-oci.com", {} }, + .{ "*.ocs.customer-oci.com", {} }, + .{ "*.oraclecloudapps.com", {} }, + .{ "*.oraclegovcloudapps.com", {} }, + .{ "*.oraclegovcloudapps.uk", {} }, + .{ "tech.orange", {} }, + .{ "can.re", {} }, + .{ "authgear-staging.com", {} }, + .{ "authgearapps.com", {} }, + .{ "skygearapp.com", {} }, + .{ "outsystemscloud.com", {} }, + .{ "*.hosting.ovh.net", {} }, + .{ "*.webpaas.ovh.net", {} }, + .{ "ownprovider.com", {} }, + .{ "own.pm", {} }, + .{ "*.owo.codes", {} }, + .{ "ox.rs", {} }, + .{ "oy.lc", {} }, + .{ "pgfog.com", {} }, + .{ "pagexl.com", {} }, + .{ "gotpantheon.com", {} }, + .{ "pantheonsite.io", {} }, + .{ "*.paywhirl.com", {} }, + .{ "*.xmit.co", {} }, + .{ "xmit.dev", {} }, + .{ "madethis.site", {} }, + .{ "srv.us", {} }, + .{ "gh.srv.us", {} }, + .{ "gl.srv.us", {} }, + .{ "lk3.ru", {} }, + .{ "mypep.link", {} }, + .{ "perspecta.cloud", {} }, + .{ "on-web.fr", {} }, + .{ "*.upsun.app", {} }, + .{ "upsunapp.com", {} }, + .{ "ent.platform.sh", {} }, + .{ "eu.platform.sh", {} }, + .{ "us.platform.sh", {} }, + .{ "*.platformsh.site", {} }, + .{ "*.tst.site", {} }, + .{ "platter-app.dev", {} }, + .{ "platterp.us", {} }, + .{ "pley.games", {} }, + .{ "onporter.run", {} }, + .{ "co.bn", {} }, + .{ "postman-echo.com", {} }, + .{ "pstmn.io", {} }, + .{ "mock.pstmn.io", {} }, + .{ "httpbin.org", {} }, + .{ "prequalifyme.today", {} }, + .{ "xen.prgmr.com", {} }, + .{ "priv.at", {} }, + .{ "protonet.io", {} }, + .{ "sub.psl.hrsn.dev", {} }, + .{ "*.wc.psl.hrsn.dev", {} }, + .{ "!ignored.wc.psl.hrsn.dev", {} }, + .{ "*.sub.wc.psl.hrsn.dev", {} }, + .{ "!ignored.sub.wc.psl.hrsn.dev", {} }, + .{ "chirurgiens-dentistes-en-france.fr", {} }, + .{ "byen.site", {} }, + .{ "pubtls.org", {} }, + .{ "pythonanywhere.com", {} }, + .{ "eu.pythonanywhere.com", {} }, + .{ "qa2.com", {} }, + .{ "qcx.io", {} }, + .{ "*.sys.qcx.io", {} }, + .{ "myqnapcloud.cn", {} }, + .{ "alpha-myqnapcloud.com", {} }, + .{ "dev-myqnapcloud.com", {} }, + .{ "mycloudnas.com", {} }, + .{ "mynascloud.com", {} }, + .{ "myqnapcloud.com", {} }, + .{ "qoto.io", {} }, + .{ "qualifioapp.com", {} }, + .{ "ladesk.com", {} }, + .{ "qbuser.com", {} }, + .{ "*.quipelements.com", {} }, + .{ "vapor.cloud", {} }, + .{ "vaporcloud.io", {} }, + .{ "rackmaze.com", {} }, + .{ "rackmaze.net", {} }, + .{ "cloudsite.builders", {} }, + .{ "myradweb.net", {} }, + .{ "servername.us", {} }, + .{ "web.in", {} }, + .{ "in.net", {} }, + .{ "myrdbx.io", {} }, + .{ "site.rb-hosting.io", {} }, + .{ "*.on-rancher.cloud", {} }, + .{ "*.on-k3s.io", {} }, + .{ "*.on-rio.io", {} }, + .{ "ravpage.co.il", {} }, + .{ "readthedocs-hosted.com", {} }, + .{ "readthedocs.io", {} }, + .{ "rhcloud.com", {} }, + .{ "instances.spawn.cc", {} }, + .{ "onrender.com", {} }, + .{ "app.render.com", {} }, + .{ "replit.app", {} }, + .{ "id.replit.app", {} }, + .{ "firewalledreplit.co", {} }, + .{ "id.firewalledreplit.co", {} }, + .{ "repl.co", {} }, + .{ "id.repl.co", {} }, + .{ "replit.dev", {} }, + .{ "archer.replit.dev", {} }, + .{ "bones.replit.dev", {} }, + .{ "canary.replit.dev", {} }, + .{ "global.replit.dev", {} }, + .{ "hacker.replit.dev", {} }, + .{ "id.replit.dev", {} }, + .{ "janeway.replit.dev", {} }, + .{ "kim.replit.dev", {} }, + .{ "kira.replit.dev", {} }, + .{ "kirk.replit.dev", {} }, + .{ "odo.replit.dev", {} }, + .{ "paris.replit.dev", {} }, + .{ "picard.replit.dev", {} }, + .{ "pike.replit.dev", {} }, + .{ "prerelease.replit.dev", {} }, + .{ "reed.replit.dev", {} }, + .{ "riker.replit.dev", {} }, + .{ "sisko.replit.dev", {} }, + .{ "spock.replit.dev", {} }, + .{ "staging.replit.dev", {} }, + .{ "sulu.replit.dev", {} }, + .{ "tarpit.replit.dev", {} }, + .{ "teams.replit.dev", {} }, + .{ "tucker.replit.dev", {} }, + .{ "wesley.replit.dev", {} }, + .{ "worf.replit.dev", {} }, + .{ "repl.run", {} }, + .{ "resindevice.io", {} }, + .{ "devices.resinstaging.io", {} }, + .{ "hzc.io", {} }, + .{ "adimo.co.uk", {} }, + .{ "itcouldbewor.se", {} }, + .{ "aus.basketball", {} }, + .{ "nz.basketball", {} }, + .{ "subsc-pay.com", {} }, + .{ "subsc-pay.net", {} }, + .{ "git-pages.rit.edu", {} }, + .{ "rocky.page", {} }, + .{ "rub.de", {} }, + .{ "ruhr-uni-bochum.de", {} }, + .{ "io.noc.ruhr-uni-bochum.de", {} }, + .{ "биз.рус", {} }, + .{ "ком.рус", {} }, + .{ "крым.рус", {} }, + .{ "мир.рус", {} }, + .{ "мск.рус", {} }, + .{ "орг.рус", {} }, + .{ "самара.рус", {} }, + .{ "сочи.рус", {} }, + .{ "спб.рус", {} }, + .{ "я.рус", {} }, + .{ "ras.ru", {} }, + .{ "nyat.app", {} }, + .{ "180r.com", {} }, + .{ "dojin.com", {} }, + .{ "sakuratan.com", {} }, + .{ "sakuraweb.com", {} }, + .{ "x0.com", {} }, + .{ "2-d.jp", {} }, + .{ "bona.jp", {} }, + .{ "crap.jp", {} }, + .{ "daynight.jp", {} }, + .{ "eek.jp", {} }, + .{ "flop.jp", {} }, + .{ "halfmoon.jp", {} }, + .{ "jeez.jp", {} }, + .{ "matrix.jp", {} }, + .{ "mimoza.jp", {} }, + .{ "ivory.ne.jp", {} }, + .{ "mail-box.ne.jp", {} }, + .{ "mints.ne.jp", {} }, + .{ "mokuren.ne.jp", {} }, + .{ "opal.ne.jp", {} }, + .{ "sakura.ne.jp", {} }, + .{ "sumomo.ne.jp", {} }, + .{ "topaz.ne.jp", {} }, + .{ "netgamers.jp", {} }, + .{ "nyanta.jp", {} }, + .{ "o0o0.jp", {} }, + .{ "rdy.jp", {} }, + .{ "rgr.jp", {} }, + .{ "rulez.jp", {} }, + .{ "s3.isk01.sakurastorage.jp", {} }, + .{ "s3.isk02.sakurastorage.jp", {} }, + .{ "saloon.jp", {} }, + .{ "sblo.jp", {} }, + .{ "skr.jp", {} }, + .{ "tank.jp", {} }, + .{ "uh-oh.jp", {} }, + .{ "undo.jp", {} }, + .{ "rs.webaccel.jp", {} }, + .{ "user.webaccel.jp", {} }, + .{ "websozai.jp", {} }, + .{ "xii.jp", {} }, + .{ "squares.net", {} }, + .{ "jpn.org", {} }, + .{ "kirara.st", {} }, + .{ "x0.to", {} }, + .{ "from.tv", {} }, + .{ "sakura.tv", {} }, + .{ "*.builder.code.com", {} }, + .{ "*.dev-builder.code.com", {} }, + .{ "*.stg-builder.code.com", {} }, + .{ "*.001.test.code-builder-stg.platform.salesforce.com", {} }, + .{ "*.d.crm.dev", {} }, + .{ "*.w.crm.dev", {} }, + .{ "*.wa.crm.dev", {} }, + .{ "*.wb.crm.dev", {} }, + .{ "*.wc.crm.dev", {} }, + .{ "*.wd.crm.dev", {} }, + .{ "*.we.crm.dev", {} }, + .{ "*.wf.crm.dev", {} }, + .{ "sandcats.io", {} }, + .{ "logoip.com", {} }, + .{ "logoip.de", {} }, + .{ "fr-par-1.baremetal.scw.cloud", {} }, + .{ "fr-par-2.baremetal.scw.cloud", {} }, + .{ "nl-ams-1.baremetal.scw.cloud", {} }, + .{ "cockpit.fr-par.scw.cloud", {} }, + .{ "fnc.fr-par.scw.cloud", {} }, + .{ "functions.fnc.fr-par.scw.cloud", {} }, + .{ "k8s.fr-par.scw.cloud", {} }, + .{ "nodes.k8s.fr-par.scw.cloud", {} }, + .{ "s3.fr-par.scw.cloud", {} }, + .{ "s3-website.fr-par.scw.cloud", {} }, + .{ "whm.fr-par.scw.cloud", {} }, + .{ "priv.instances.scw.cloud", {} }, + .{ "pub.instances.scw.cloud", {} }, + .{ "k8s.scw.cloud", {} }, + .{ "cockpit.nl-ams.scw.cloud", {} }, + .{ "k8s.nl-ams.scw.cloud", {} }, + .{ "nodes.k8s.nl-ams.scw.cloud", {} }, + .{ "s3.nl-ams.scw.cloud", {} }, + .{ "s3-website.nl-ams.scw.cloud", {} }, + .{ "whm.nl-ams.scw.cloud", {} }, + .{ "cockpit.pl-waw.scw.cloud", {} }, + .{ "k8s.pl-waw.scw.cloud", {} }, + .{ "nodes.k8s.pl-waw.scw.cloud", {} }, + .{ "s3.pl-waw.scw.cloud", {} }, + .{ "s3-website.pl-waw.scw.cloud", {} }, + .{ "scalebook.scw.cloud", {} }, + .{ "smartlabeling.scw.cloud", {} }, + .{ "dedibox.fr", {} }, + .{ "schokokeks.net", {} }, + .{ "gov.scot", {} }, + .{ "service.gov.scot", {} }, + .{ "scrysec.com", {} }, + .{ "client.scrypted.io", {} }, + .{ "firewall-gateway.com", {} }, + .{ "firewall-gateway.de", {} }, + .{ "my-gateway.de", {} }, + .{ "my-router.de", {} }, + .{ "spdns.de", {} }, + .{ "spdns.eu", {} }, + .{ "firewall-gateway.net", {} }, + .{ "my-firewall.org", {} }, + .{ "myfirewall.org", {} }, + .{ "spdns.org", {} }, + .{ "seidat.net", {} }, + .{ "sellfy.store", {} }, + .{ "minisite.ms", {} }, + .{ "senseering.net", {} }, + .{ "servebolt.cloud", {} }, + .{ "biz.ua", {} }, + .{ "co.ua", {} }, + .{ "pp.ua", {} }, + .{ "as.sh.cn", {} }, + .{ "sheezy.games", {} }, + .{ "myshopblocks.com", {} }, + .{ "myshopify.com", {} }, + .{ "shopitsite.com", {} }, + .{ "shopware.shop", {} }, + .{ "shopware.store", {} }, + .{ "mo-siemens.io", {} }, + .{ "1kapp.com", {} }, + .{ "appchizi.com", {} }, + .{ "applinzi.com", {} }, + .{ "sinaapp.com", {} }, + .{ "vipsinaapp.com", {} }, + .{ "siteleaf.net", {} }, + .{ "small-web.org", {} }, + .{ "aeroport.fr", {} }, + .{ "avocat.fr", {} }, + .{ "chambagri.fr", {} }, + .{ "chirurgiens-dentistes.fr", {} }, + .{ "experts-comptables.fr", {} }, + .{ "medecin.fr", {} }, + .{ "notaires.fr", {} }, + .{ "pharmacien.fr", {} }, + .{ "port.fr", {} }, + .{ "veterinaire.fr", {} }, + .{ "vp4.me", {} }, + .{ "*.snowflake.app", {} }, + .{ "*.privatelink.snowflake.app", {} }, + .{ "streamlit.app", {} }, + .{ "streamlitapp.com", {} }, + .{ "try-snowplow.com", {} }, + .{ "mafelo.net", {} }, + .{ "playstation-cloud.com", {} }, + .{ "srht.site", {} }, + .{ "apps.lair.io", {} }, + .{ "*.stolos.io", {} }, + .{ "ind.mom", {} }, + .{ "customer.speedpartner.de", {} }, + .{ "myspreadshop.at", {} }, + .{ "myspreadshop.com.au", {} }, + .{ "myspreadshop.be", {} }, + .{ "myspreadshop.ca", {} }, + .{ "myspreadshop.ch", {} }, + .{ "myspreadshop.com", {} }, + .{ "myspreadshop.de", {} }, + .{ "myspreadshop.dk", {} }, + .{ "myspreadshop.es", {} }, + .{ "myspreadshop.fi", {} }, + .{ "myspreadshop.fr", {} }, + .{ "myspreadshop.ie", {} }, + .{ "myspreadshop.it", {} }, + .{ "myspreadshop.net", {} }, + .{ "myspreadshop.nl", {} }, + .{ "myspreadshop.no", {} }, + .{ "myspreadshop.pl", {} }, + .{ "myspreadshop.se", {} }, + .{ "myspreadshop.co.uk", {} }, + .{ "w-corp-staticblitz.com", {} }, + .{ "w-credentialless-staticblitz.com", {} }, + .{ "w-staticblitz.com", {} }, + .{ "stackhero-network.com", {} }, + .{ "runs.onstackit.cloud", {} }, + .{ "stackit.gg", {} }, + .{ "stackit.rocks", {} }, + .{ "stackit.run", {} }, + .{ "stackit.zone", {} }, + .{ "musician.io", {} }, + .{ "novecore.site", {} }, + .{ "api.stdlib.com", {} }, + .{ "feedback.ac", {} }, + .{ "forms.ac", {} }, + .{ "assessments.cx", {} }, + .{ "calculators.cx", {} }, + .{ "funnels.cx", {} }, + .{ "paynow.cx", {} }, + .{ "quizzes.cx", {} }, + .{ "researched.cx", {} }, + .{ "tests.cx", {} }, + .{ "surveys.so", {} }, + .{ "storebase.store", {} }, + .{ "storipress.app", {} }, + .{ "storj.farm", {} }, + .{ "strapiapp.com", {} }, + .{ "media.strapiapp.com", {} }, + .{ "vps-host.net", {} }, + .{ "atl.jelastic.vps-host.net", {} }, + .{ "njs.jelastic.vps-host.net", {} }, + .{ "ric.jelastic.vps-host.net", {} }, + .{ "streak-link.com", {} }, + .{ "streaklinks.com", {} }, + .{ "streakusercontent.com", {} }, + .{ "soc.srcf.net", {} }, + .{ "user.srcf.net", {} }, + .{ "utwente.io", {} }, + .{ "temp-dns.com", {} }, + .{ "supabase.co", {} }, + .{ "supabase.in", {} }, + .{ "supabase.net", {} }, + .{ "syncloud.it", {} }, + .{ "dscloud.biz", {} }, + .{ "direct.quickconnect.cn", {} }, + .{ "dsmynas.com", {} }, + .{ "familyds.com", {} }, + .{ "diskstation.me", {} }, + .{ "dscloud.me", {} }, + .{ "i234.me", {} }, + .{ "myds.me", {} }, + .{ "synology.me", {} }, + .{ "dscloud.mobi", {} }, + .{ "dsmynas.net", {} }, + .{ "familyds.net", {} }, + .{ "dsmynas.org", {} }, + .{ "familyds.org", {} }, + .{ "direct.quickconnect.to", {} }, + .{ "vpnplus.to", {} }, + .{ "mytabit.com", {} }, + .{ "mytabit.co.il", {} }, + .{ "tabitorder.co.il", {} }, + .{ "taifun-dns.de", {} }, + .{ "ts.net", {} }, + .{ "*.c.ts.net", {} }, + .{ "gda.pl", {} }, + .{ "gdansk.pl", {} }, + .{ "gdynia.pl", {} }, + .{ "med.pl", {} }, + .{ "sopot.pl", {} }, + .{ "taveusercontent.com", {} }, + .{ "p.tawk.email", {} }, + .{ "p.tawkto.email", {} }, + .{ "site.tb-hosting.com", {} }, + .{ "edugit.io", {} }, + .{ "s3.teckids.org", {} }, + .{ "telebit.app", {} }, + .{ "telebit.io", {} }, + .{ "*.telebit.xyz", {} }, + .{ "*.firenet.ch", {} }, + .{ "*.svc.firenet.ch", {} }, + .{ "reservd.com", {} }, + .{ "thingdustdata.com", {} }, + .{ "cust.dev.thingdust.io", {} }, + .{ "reservd.dev.thingdust.io", {} }, + .{ "cust.disrec.thingdust.io", {} }, + .{ "reservd.disrec.thingdust.io", {} }, + .{ "cust.prod.thingdust.io", {} }, + .{ "cust.testing.thingdust.io", {} }, + .{ "reservd.testing.thingdust.io", {} }, + .{ "tickets.io", {} }, + .{ "arvo.network", {} }, + .{ "azimuth.network", {} }, + .{ "tlon.network", {} }, + .{ "torproject.net", {} }, + .{ "pages.torproject.net", {} }, + .{ "townnews-staging.com", {} }, + .{ "12hp.at", {} }, + .{ "2ix.at", {} }, + .{ "4lima.at", {} }, + .{ "lima-city.at", {} }, + .{ "12hp.ch", {} }, + .{ "2ix.ch", {} }, + .{ "4lima.ch", {} }, + .{ "lima-city.ch", {} }, + .{ "trafficplex.cloud", {} }, + .{ "de.cool", {} }, + .{ "12hp.de", {} }, + .{ "2ix.de", {} }, + .{ "4lima.de", {} }, + .{ "lima-city.de", {} }, + .{ "1337.pictures", {} }, + .{ "clan.rip", {} }, + .{ "lima-city.rocks", {} }, + .{ "webspace.rocks", {} }, + .{ "lima.zone", {} }, + .{ "*.transurl.be", {} }, + .{ "*.transurl.eu", {} }, + .{ "site.transip.me", {} }, + .{ "*.transurl.nl", {} }, + .{ "tuxfamily.org", {} }, + .{ "dd-dns.de", {} }, + .{ "dray-dns.de", {} }, + .{ "draydns.de", {} }, + .{ "dyn-vpn.de", {} }, + .{ "dynvpn.de", {} }, + .{ "mein-vigor.de", {} }, + .{ "my-vigor.de", {} }, + .{ "my-wan.de", {} }, + .{ "syno-ds.de", {} }, + .{ "synology-diskstation.de", {} }, + .{ "synology-ds.de", {} }, + .{ "diskstation.eu", {} }, + .{ "diskstation.org", {} }, + .{ "typedream.app", {} }, + .{ "pro.typeform.com", {} }, + .{ "*.uberspace.de", {} }, + .{ "uber.space", {} }, + .{ "hk.com", {} }, + .{ "inc.hk", {} }, + .{ "ltd.hk", {} }, + .{ "hk.org", {} }, + .{ "it.com", {} }, + .{ "unison-services.cloud", {} }, + .{ "virtual-user.de", {} }, + .{ "virtualuser.de", {} }, + .{ "obj.ag", {} }, + .{ "name.pm", {} }, + .{ "sch.tf", {} }, + .{ "biz.wf", {} }, + .{ "sch.wf", {} }, + .{ "org.yt", {} }, + .{ "rs.ba", {} }, + .{ "bielsko.pl", {} }, + .{ "urown.cloud", {} }, + .{ "dnsupdate.info", {} }, + .{ "us.org", {} }, + .{ "v.ua", {} }, + .{ "express.val.run", {} }, + .{ "web.val.run", {} }, + .{ "vercel.app", {} }, + .{ "v0.build", {} }, + .{ "vercel.dev", {} }, + .{ "vusercontent.net", {} }, + .{ "now.sh", {} }, + .{ "2038.io", {} }, + .{ "router.management", {} }, + .{ "v-info.info", {} }, + .{ "voorloper.cloud", {} }, + .{ "*.vultrobjects.com", {} }, + .{ "wafflecell.com", {} }, + .{ "webflow.io", {} }, + .{ "webflowtest.io", {} }, + .{ "*.webhare.dev", {} }, + .{ "bookonline.app", {} }, + .{ "hotelwithflight.com", {} }, + .{ "reserve-online.com", {} }, + .{ "reserve-online.net", {} }, + .{ "cprapid.com", {} }, + .{ "pleskns.com", {} }, + .{ "wp2.host", {} }, + .{ "pdns.page", {} }, + .{ "plesk.page", {} }, + .{ "wpsquared.site", {} }, + .{ "*.wadl.top", {} }, + .{ "remotewd.com", {} }, + .{ "box.ca", {} }, + .{ "pages.wiardweb.com", {} }, + .{ "toolforge.org", {} }, + .{ "wmcloud.org", {} }, + .{ "wmflabs.org", {} }, + .{ "wdh.app", {} }, + .{ "panel.gg", {} }, + .{ "daemon.panel.gg", {} }, + .{ "wixsite.com", {} }, + .{ "wixstudio.com", {} }, + .{ "editorx.io", {} }, + .{ "wixstudio.io", {} }, + .{ "wix.run", {} }, + .{ "messwithdns.com", {} }, + .{ "woltlab-demo.com", {} }, + .{ "myforum.community", {} }, + .{ "community-pro.de", {} }, + .{ "diskussionsbereich.de", {} }, + .{ "community-pro.net", {} }, + .{ "meinforum.net", {} }, + .{ "affinitylottery.org.uk", {} }, + .{ "raffleentry.org.uk", {} }, + .{ "weeklylottery.org.uk", {} }, + .{ "wpenginepowered.com", {} }, + .{ "js.wpenginepowered.com", {} }, + .{ "half.host", {} }, + .{ "xnbay.com", {} }, + .{ "u2.xnbay.com", {} }, + .{ "u2-local.xnbay.com", {} }, + .{ "cistron.nl", {} }, + .{ "demon.nl", {} }, + .{ "xs4all.space", {} }, + .{ "yandexcloud.net", {} }, + .{ "storage.yandexcloud.net", {} }, + .{ "website.yandexcloud.net", {} }, + .{ "official.academy", {} }, + .{ "yolasite.com", {} }, + .{ "ynh.fr", {} }, + .{ "nohost.me", {} }, + .{ "noho.st", {} }, + .{ "za.net", {} }, + .{ "za.org", {} }, + .{ "zap.cloud", {} }, + .{ "zeabur.app", {} }, + .{ "*.zerops.app", {} }, + .{ "bss.design", {} }, + .{ "basicserver.io", {} }, + .{ "virtualserver.io", {} }, + .{ "enterprisecloud.nu", {} }, +}); diff --git a/src/data/public_suffix_list_gen.go b/src/data/public_suffix_list_gen.go new file mode 100644 index 000000000..137df6df6 --- /dev/null +++ b/src/data/public_suffix_list_gen.go @@ -0,0 +1,42 @@ +package main + +import ( + "bufio" + "fmt" + "net/http" + "strings" +) + +func main() { + resp, err := http.Get("https://publicsuffix.org/list/public_suffix_list.dat") + if err != nil { + panic(err) + } + defer resp.Body.Close() + + var domains []string + + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if len(line) == 0 || strings.HasPrefix(line, "//") { + continue + } + + domains = append(domains, line) + } + + lookup := + "const std = @import(\"std\");\n\n" + + "pub fn lookup(value: []const u8) bool {\n" + + " return public_suffix_list.has(value);\n" + + "}\n" + fmt.Println(lookup) + + fmt.Println("const public_suffix_list = std.StaticStringMap(void).initComptime([_]struct { []const u8, void }{") + for _, domain := range domains { + fmt.Printf(` .{ "%s", {} },`, domain) + fmt.Println() + } + fmt.Println("});") +} diff --git a/src/storage/cookie.zig b/src/storage/cookie.zig index 609b2d330..d9bebb36e 100644 --- a/src/storage/cookie.zig +++ b/src/storage/cookie.zig @@ -4,10 +4,11 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const DateTime = @import("../datetime.zig").DateTime; +const public_suffix_list = @import("../data/public_suffix_list.zig").lookup; pub const Jar = struct { allocator: Allocator, - cookies: std.ArrayListUnmanaged(u8), + cookies: std.ArrayListUnmanaged(Cookie), pub fn init(allocator: Allocator) Jar { return .{ @@ -23,47 +24,174 @@ pub const Jar = struct { self.cookies.deinit(self.allocator); } + pub fn add( + self: *Jar, + cookie: Cookie, + request_time: i64, + ) !void { + const is_expired = isCookieExpired(&cookie, request_time); + defer if (is_expired) { + cookie.deinit(); + }; + + for (self.cookies.items, 0..) |*c, i| { + if (areCookiesEqual(&cookie, c)) { + c.deinit(); + if (is_expired) { + _ = self.cookies.swapRemove(i); + } else { + self.cookies.items[i] = cookie; + } + return; + } + } + + if (!is_expired) { + try self.cookies.append(self.allocator, cookie); + } + } + pub fn forRequest( - self: *const Jar, + self: *Jar, allocator: Allocator, - request_start: i64, + request_time: i64, origin_uri: ?Uri, target_uri: Uri, navitation: bool, ) !CookieList { - const is_secure = std.mem.eql(u8, target_uri.scheme, "https"); + const target_path = target_uri.path.percent_encoded; const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded; + const same_site = try areSameSite(origin_uri, target_host); + const is_secure = std.mem.eql(u8, target_uri.scheme, "https"); + + var matching: std.ArrayListUnmanaged(*Cookie) = .{}; var i: usize = 0; var cookies = self.cookies.items; while (i < cookies.len) { const cookie = &cookies[i]; - if (cookie.isExpired(request_start)) { - self.swapRemove(i); + + if (isCookieExpired(cookie, request_time)) { + cookie.deinit(); + _ = self.cookies.swapRemove(i); // don't increment i ! continue; } i += 1; if (is_secure == false and cookie.secure) { + // secure cookie can only be sent over HTTPs continue; } - // www.google.com + if (same_site == false) { + // If we aren't on the "same site" (matching 2nd level domain + // taking into account public suffix list), then the cookie + // can only be sent if cookie.same_site == .none, or if + // we're navigating to (as opposed to, say, loading an image) + // and cookie.same_site == .lax + switch (cookie.same_site) { + .strict => continue, + .lax => if (navitation == false) continue, + .none => {}, + } + } - if (navitation == false and cookie.same_site != .strict) { - continue; + { + const domain = cookie.domain; + if (domain[0] == '.') { + // when explicitly set, the domain + // 1 - always starts with a . + // 2 - always is a suffix match (or examlpe) + if (std.mem.eql(u8, target_host, domain[1..]) == false and std.mem.endsWith(u8, target_host, domain) == false) { + continue; + } + } else if (std.mem.eql(u8, target_host, domain) == false) { + // when Domain=XYX isn't specified, it's an exact match only + continue; + } } + + { + const path = cookie.path; + if (path[path.len - 1] == '/') { + // If our cookie path is doc/ + // Then we can only match if the target path starts with doc/ + if (std.mem.startsWith(u8, target_path, path) == false) { + continue; + } + } else { + // Our cookie path is something like /hello + if (std.mem.startsWith(u8, target_path, path) == false) { + // The target path has to either be /hello (it isn't) + continue; + } else if (target_path.len < path.len or (target_path.len > path.len and target_path[path.len] != '/')) { + // Or it has to be something like /hello/* (it isn't) + // it isn't! + continue; + } + } + } + // we have a match! + try matching.append(allocator, cookie); } + + return .{ ._cookies = matching }; } }; -// abc.lightpanda.io is the same site as lightpanda.io or 123.lightpanda.io -// or spice.123.lightpanda.io +pub const CookieList = struct { + _cookies: std.ArrayListUnmanaged(*Cookie), + + pub fn deinit(self: *CookieList, allocator: Allocator) void { + self._cookies.deinit(allocator); + } + + pub fn cookies(self: *const CookieList) []*Cookie { + return self._cookies.items; + } +}; + +fn isCookieExpired(cookie: *const Cookie, now: i64) bool { + const ce = cookie.expires orelse return false; + return ce <= now; +} + +fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool { + if (std.mem.eql(u8, a.name, b.name) == false) { + return false; + } + if (std.mem.eql(u8, a.domain, b.domain) == false) { + return false; + } + if (std.mem.eql(u8, a.path, b.path) == false) { + return false; + } + return true; +} + fn areSameSite(origin_uri_: ?std.Uri, target_host: []const u8) !bool { const origin_uri = origin_uri_ orelse return true; const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded; + + // common case + if (std.mem.eql(u8, target_host, origin_host)) { + return true; + } + + return std.mem.eql(u8, findSecondLevelDomain(target_host), findSecondLevelDomain(origin_host)); +} + +fn findSecondLevelDomain(host: []const u8) []const u8 { + var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host; + while (true) { + i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host; + const strip = i + 1; + if (public_suffix_list(host[strip..]) == false) { + return host[strip..]; + } + } } pub const Cookie = struct { @@ -275,6 +403,215 @@ fn trimRight(str: []const u8) []const u8 { } const testing = @import("../testing.zig"); +test "cookie: findSecondLevelDomain" { + const cases = [_]struct { []const u8, []const u8 }{ + .{ "", "" }, + .{ "com", "com" }, + .{ "lightpanda.io", "lightpanda.io" }, + .{ "lightpanda.io", "test.lightpanda.io" }, + .{ "lightpanda.io", "first.test.lightpanda.io" }, + .{ "www.gov.uk", "www.gov.uk" }, + .{ "stats.gov.uk", "www.stats.gov.uk" }, + .{ "api.gov.uk", "api.gov.uk" }, + .{ "dev.api.gov.uk", "dev.api.gov.uk" }, + .{ "dev.api.gov.uk", "1.dev.api.gov.uk" }, + }; + for (cases) |c| { + try testing.expectEqual(c.@"0", findSecondLevelDomain(c.@"1")); + } +} + +test "Jar: add" { + const expectCookies = struct { + fn expect(expected: []const struct { []const u8, []const u8 }, jar: Jar) !void { + try testing.expectEqual(expected.len, jar.cookies.items.len); + LOOP: for (expected) |e| { + for (jar.cookies.items) |c| { + if (std.mem.eql(u8, e.@"0", c.name) and std.mem.eql(u8, e.@"1", c.value)) { + continue :LOOP; + } + } + std.debug.print("Cookie ({s}={s}) not found", .{ e.@"0", e.@"1" }); + return error.CookieNotFound; + } + } + }.expect; + + const now = std.time.timestamp(); + + var jar = Jar.init(testing.allocator); + defer jar.deinit(); + try expectCookies(&.{}, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "over=9000;Max-Age=0"), now); + try expectCookies(&.{}, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "over=9000"), now); + try expectCookies(&.{.{ "over", "9000" }}, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "over=9000!!"), now); + try expectCookies(&.{.{ "over", "9000!!" }}, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "spice=flow"), now); + try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flow" } }, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "spice=flows;Path=/"), now); + try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" } }, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "over=9001;Path=/other"), now); + try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" } }, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "over=9002;Path=/;Domain=lightpanda.io"), now); + try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" }, .{ "over", "9002" } }, jar); + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "over=x;Path=/other;Max-Age=-200"), now); + try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9002" } }, jar); +} + +test "Jar: forRequest" { + const expectCookies = struct { + fn expect(expected: []const []const u8, list: *CookieList) !void { + defer list.deinit(testing.allocator); + const acutal_cookies = list._cookies.items; + + try testing.expectEqual(expected.len, acutal_cookies.len); + LOOP: for (expected) |e| { + for (acutal_cookies) |c| { + if (std.mem.eql(u8, e, c.name)) { + continue :LOOP; + } + } + std.debug.print("Cookie '{s}' not found", .{e}); + return error.CookieNotFound; + } + } + }.expect; + + const now = std.time.timestamp(); + + var jar = Jar.init(testing.allocator); + defer jar.deinit(); + + const test_uri_2 = Uri.parse("http://test.lightpanda.io/") catch unreachable; + + { + // test with no cookies + var matches = try jar.forRequest(testing.allocator, now, test_uri, test_uri, true); + try expectCookies(&.{}, &matches); + } + + try jar.add(try Cookie.parse(testing.allocator, test_uri, "global1=1"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "global2=2;Max-Age=30"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "path1=3;Path=/about"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "path2=4;Path=/docs/"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "secure=5;Secure"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitenone=6;SameSite=None;Path=/x/"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitelax=7;SameSite=Lax;Path=/x/"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri_2, "domain1=9;domain=test.lightpanda.io"), now); + + { + // nothing fancy here + var matches = try jar.forRequest(testing.allocator, now, test_uri, test_uri, true); + try expectCookies(&.{ "global1", "global2" }, &matches); + } + + { + // matching path without trailing / + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/about"), true); + try expectCookies(&.{ "global1", "global2", "path1" }, &matches); + } + + { + // incomplete prefix path + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/abou"), true); + try expectCookies(&.{ "global1", "global2" }, &matches); + } + + { + // path doesn't match + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/aboutus"), true); + try expectCookies(&.{ "global1", "global2" }, &matches); + } + + { + // path doesn't match cookie directory + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/docs"), true); + try expectCookies(&.{ "global1", "global2" }, &matches); + } + + { + // exact directory match + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/docs/"), true); + try expectCookies(&.{ "global1", "global2", "path2" }, &matches); + } + + { + // sub directory match + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/docs/more"), true); + try expectCookies(&.{ "global1", "global2", "path2" }, &matches); + } + + { + // secure + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("https://lightpanda.io/"), true); + try expectCookies(&.{ "global1", "global2", "secure" }, &matches); + } + + { + // navigational cross domain + var matches = try jar.forRequest(testing.allocator, now, try std.Uri.parse("http://example.com/"), try std.Uri.parse("http://lightpanda.io/x/"), true); + try expectCookies(&.{ "global1", "global2", "sitenone", "sitelax" }, &matches); + } + + { + // non-navigational cross domain + var matches = try jar.forRequest(testing.allocator, now, try std.Uri.parse("http://example.com/"), try std.Uri.parse("http://lightpanda.io/x/"), false); + try expectCookies(&.{"sitenone"}, &matches); + } + + { + // non-navigational same origin + var matches = try jar.forRequest( + testing.allocator, + now, + try std.Uri.parse("http://lightpanda.io/"), + try std.Uri.parse("http://lightpanda.io/x/"), + false, + ); + try expectCookies(&.{ "global1", "global2", "sitenone", "sitelax", "sitestrict" }, &matches); + } + + { + // exact domain match + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://test.lightpanda.io/"), true); + try expectCookies(&.{"domain1"}, &matches); + } + + { + // domain suffix match + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://1.test.lightpanda.io/"), true); + try expectCookies(&.{"domain1"}, &matches); + } + + { + // non-matching domain + var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://other.lightpanda.io/"), true); + try expectCookies(&.{}, &matches); + } + + { + // cookie has expired + const l = jar.cookies.items.len; + var matches = try jar.forRequest(testing.allocator, now + 100, test_uri, test_uri, true); + try expectCookies(&.{"global1"}, &matches); + try testing.expectEqual(l - 1, jar.cookies.items.len); + } + + // If you add more cases after this point, note that the above test removes + // the 'global2' cookie +} + test "Cookie: parse key=value" { try expectError(error.Empty, null, ""); try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' }); @@ -461,7 +798,7 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u } fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void { - const uri = if (url) |u| try Uri.parse(u) else dummy_test_uri; + const uri = if (url) |u| try Uri.parse(u) else test_uri; var cookie = try Cookie.parse(testing.allocator, uri, set_cookie); defer cookie.deinit(); @@ -475,8 +812,8 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) } fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void { - const uri = if (url) |u| try Uri.parse(u) else dummy_test_uri; + const uri = if (url) |u| try Uri.parse(u) else test_uri; try testing.expectError(expected, Cookie.parse(testing.allocator, uri, set_cookie)); } -const dummy_test_uri = Uri.parse("http://lightpanda.io/") catch unreachable; +const test_uri = Uri.parse("http://lightpanda.io/") catch unreachable; From 467442c34e6ead594cdcbb8dc837c78e5524cb2f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Feb 2025 16:47:39 +0800 Subject: [PATCH 10/13] Cookie with SameSite=None is only valid when Secure --- src/storage/cookie.zig | 157 ++++++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 27 deletions(-) diff --git a/src/storage/cookie.zig b/src/storage/cookie.zig index d9bebb36e..01584d86a 100644 --- a/src/storage/cookie.zig +++ b/src/storage/cookie.zig @@ -244,9 +244,6 @@ pub const Cookie = struct { } } - var arena = ArenaAllocator.init(allocator); - errdefer arena.deinit(); - const cookie_name, const cookie_value, const rest = parseNameValue(str) catch { return error.InvalidNameValue; }; @@ -322,6 +319,12 @@ pub const Cookie = struct { } } + if (same_site == .none and secure == null) { + return error.InsecureSameSite; + } + + var arena = ArenaAllocator.init(allocator); + errdefer arena.deinit(); const aa = arena.allocator(); const owned_name = try aa.dupe(u8, cookie_name); const owned_value = try aa.dupe(u8, cookie_value); @@ -505,7 +508,7 @@ test "Jar: forRequest" { try jar.add(try Cookie.parse(testing.allocator, test_uri, "path1=3;Path=/about"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "path2=4;Path=/docs/"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "secure=5;Secure"), now); - try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitenone=6;SameSite=None;Path=/x/"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitenone=6;SameSite=None;Path=/x/;Secure"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitelax=7;SameSite=Lax;Path=/x/"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri_2, "domain1=9;domain=test.lightpanda.io"), now); @@ -518,55 +521,133 @@ test "Jar: forRequest" { { // matching path without trailing / - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/about"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://lightpanda.io/about"), + true, + ); try expectCookies(&.{ "global1", "global2", "path1" }, &matches); } { // incomplete prefix path - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/abou"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://lightpanda.io/abou"), + true, + ); try expectCookies(&.{ "global1", "global2" }, &matches); } { // path doesn't match - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/aboutus"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://lightpanda.io/aboutus"), + true, + ); try expectCookies(&.{ "global1", "global2" }, &matches); } { // path doesn't match cookie directory - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/docs"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://lightpanda.io/docs"), + true, + ); try expectCookies(&.{ "global1", "global2" }, &matches); } { // exact directory match - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/docs/"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://lightpanda.io/docs/"), + true, + ); try expectCookies(&.{ "global1", "global2", "path2" }, &matches); } { // sub directory match - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://lightpanda.io/docs/more"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://lightpanda.io/docs/more"), + true, + ); try expectCookies(&.{ "global1", "global2", "path2" }, &matches); } { // secure - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("https://lightpanda.io/"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("https://lightpanda.io/"), + true, + ); try expectCookies(&.{ "global1", "global2", "secure" }, &matches); } { - // navigational cross domain - var matches = try jar.forRequest(testing.allocator, now, try std.Uri.parse("http://example.com/"), try std.Uri.parse("http://lightpanda.io/x/"), true); - try expectCookies(&.{ "global1", "global2", "sitenone", "sitelax" }, &matches); + // navigational cross domain, secure + var matches = try jar.forRequest( + testing.allocator, + now, + try std.Uri.parse("https://example.com/"), + try std.Uri.parse("https://lightpanda.io/x/"), + true, + ); + try expectCookies(&.{ "global1", "global2", "sitenone", "sitelax", "secure" }, &matches); + } + + { + // navigational cross domain, insecure + var matches = try jar.forRequest( + testing.allocator, + now, + try std.Uri.parse("http://example.com/"), + try std.Uri.parse("http://lightpanda.io/x/"), + true, + ); + try expectCookies(&.{ "global1", "global2", "sitelax" }, &matches); } { - // non-navigational cross domain - var matches = try jar.forRequest(testing.allocator, now, try std.Uri.parse("http://example.com/"), try std.Uri.parse("http://lightpanda.io/x/"), false); + // non-navigational cross domain, insecure + var matches = try jar.forRequest( + testing.allocator, + now, + try std.Uri.parse("http://example.com/"), + try std.Uri.parse("http://lightpanda.io/x/"), + false, + ); + try expectCookies(&.{}, &matches); + } + + { + // non-navigational cross domain, secure + var matches = try jar.forRequest( + testing.allocator, + now, + try std.Uri.parse("https://example.com/"), + try std.Uri.parse("https://lightpanda.io/x/"), + false, + ); try expectCookies(&.{"sitenone"}, &matches); } @@ -579,24 +660,42 @@ test "Jar: forRequest" { try std.Uri.parse("http://lightpanda.io/x/"), false, ); - try expectCookies(&.{ "global1", "global2", "sitenone", "sitelax", "sitestrict" }, &matches); + try expectCookies(&.{ "global1", "global2", "sitelax", "sitestrict" }, &matches); } { // exact domain match - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://test.lightpanda.io/"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://test.lightpanda.io/"), + true, + ); try expectCookies(&.{"domain1"}, &matches); } { // domain suffix match - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://1.test.lightpanda.io/"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://1.test.lightpanda.io/"), + true, + ); try expectCookies(&.{"domain1"}, &matches); } { // non-matching domain - var matches = try jar.forRequest(testing.allocator, now, test_uri, try std.Uri.parse("http://other.lightpanda.io/"), true); + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://other.lightpanda.io/"), + true, + ); try expectCookies(&.{}, &matches); } @@ -677,7 +776,7 @@ test "Cookie: parse secure" { try expectAttribute(.{ .secure = true }, null, "b; seCUre=Off "); } -test "Cookie: parse httponly" { +test "Cookie: parse HttpOnly" { try expectAttribute(.{ .http_only = false }, null, "b"); try expectAttribute(.{ .http_only = false }, null, "b;HttpOnly0"); try expectAttribute(.{ .http_only = false }, null, "b;H ttpOnly"); @@ -689,24 +788,28 @@ test "Cookie: parse httponly" { try expectAttribute(.{ .http_only = true }, null, "b; HttpOnly=Off "); } -test "Cookie: parse strict" { +test "Cookie: parse SameSite" { try expectAttribute(.{ .same_site = .lax }, null, "b;samesite"); try expectAttribute(.{ .same_site = .lax }, null, "b;samesite=lax"); try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Lax "); try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Other "); try expectAttribute(.{ .same_site = .lax }, null, "b; SameSite=Nope "); - try expectAttribute(.{ .same_site = .none }, null, "b; samesite=none "); - try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None "); - try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None;"); - try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None"); + // SameSite=none is only valid when Secure is set. The whole cookie is + // rejected otherwise + try expectError(error.InsecureSameSite, null, "b;samesite=none"); + try expectError(error.InsecureSameSite, null, "b;SameSite=None"); + try expectAttribute(.{ .same_site = .none }, null, "b; samesite=none; secure "); + try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None ; SECURE"); + try expectAttribute(.{ .same_site = .none }, null, "b;Secure; SameSite=None"); + try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=None; Secure"); try expectAttribute(.{ .same_site = .strict }, null, "b; samesite=Strict "); try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite= STRICT "); try expectAttribute(.{ .same_site = .strict }, null, "b; SameSITE=strict;"); try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite=Strict"); - try expectAttribute(.{ .same_site = .none }, null, "b; SameSite=Strict; SameSite=lax; SameSite=NONE"); + try expectAttribute(.{ .same_site = .strict }, null, "b; SameSite=None; SameSite=lax; SameSite=Strict"); } test "Cookie: parse max-age" { From 353964bb2fad342e8971a8f86dd7b2bd8d36f339 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 4 Mar 2025 19:46:36 +0800 Subject: [PATCH 11/13] fix typo, improve comment, add 1 test case --- src/storage/cookie.zig | 45 +++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/storage/cookie.zig b/src/storage/cookie.zig index 01584d86a..3c4481a03 100644 --- a/src/storage/cookie.zig +++ b/src/storage/cookie.zig @@ -57,7 +57,7 @@ pub const Jar = struct { request_time: i64, origin_uri: ?Uri, target_uri: Uri, - navitation: bool, + navigation: bool, ) !CookieList { const target_path = target_uri.path.percent_encoded; const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded; @@ -93,7 +93,7 @@ pub const Jar = struct { // and cookie.same_site == .lax switch (cookie.same_site) { .strict => continue, - .lax => if (navitation == false) continue, + .lax => if (navigation == false) continue, .none => {}, } } @@ -101,14 +101,17 @@ pub const Jar = struct { { const domain = cookie.domain; if (domain[0] == '.') { - // when explicitly set, the domain - // 1 - always starts with a . - // 2 - always is a suffix match (or examlpe) + // When a Set-Cookie header has a Domain attribute + // Then we will _always_ prefix it with a dot, extending its + // availability to all subdomains (yes, setting the Domain + // attributes EXPANDS the domains which the cookie will be + // sent to, to always include all subdomains). if (std.mem.eql(u8, target_host, domain[1..]) == false and std.mem.endsWith(u8, target_host, domain) == false) { continue; } } else if (std.mem.eql(u8, target_host, domain) == false) { - // when Domain=XYX isn't specified, it's an exact match only + // When the Domain attribute isn't specific, then the cookie + // is only sent on an exact match. continue; } } @@ -116,8 +119,9 @@ pub const Jar = struct { { const path = cookie.path; if (path[path.len - 1] == '/') { - // If our cookie path is doc/ - // Then we can only match if the target path starts with doc/ + // If our cookie has a trailing slash, we can only match is + // the target path is a perfix. I.e., if our path is + // /doc/ we can only match /doc/* if (std.mem.startsWith(u8, target_path, path) == false) { continue; } @@ -504,7 +508,7 @@ test "Jar: forRequest" { } try jar.add(try Cookie.parse(testing.allocator, test_uri, "global1=1"), now); - try jar.add(try Cookie.parse(testing.allocator, test_uri, "global2=2;Max-Age=30"), now); + try jar.add(try Cookie.parse(testing.allocator, test_uri, "global2=2;Max-Age=30;domain=lightpanda.io"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "path1=3;Path=/about"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "path2=4;Path=/docs/"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri, "secure=5;Secure"), now); @@ -519,6 +523,19 @@ test "Jar: forRequest" { try expectCookies(&.{ "global1", "global2" }, &matches); } + { + // We have a cookie where Domain=lightpanda.io + // This should _not_ match xyxlightpanda.io + var matches = try jar.forRequest( + testing.allocator, + now, + test_uri, + try std.Uri.parse("http://anothersitelightpanda.io/"), + true, + ); + try expectCookies(&.{}, &matches); + } + { // matching path without trailing / var matches = try jar.forRequest( @@ -664,7 +681,7 @@ test "Jar: forRequest" { } { - // exact domain match + // exact domain match + suffix var matches = try jar.forRequest( testing.allocator, now, @@ -672,11 +689,11 @@ test "Jar: forRequest" { try std.Uri.parse("http://test.lightpanda.io/"), true, ); - try expectCookies(&.{"domain1"}, &matches); + try expectCookies(&.{ "global2", "domain1" }, &matches); } { - // domain suffix match + // domain suffix match + suffix var matches = try jar.forRequest( testing.allocator, now, @@ -684,7 +701,7 @@ test "Jar: forRequest" { try std.Uri.parse("http://1.test.lightpanda.io/"), true, ); - try expectCookies(&.{"domain1"}, &matches); + try expectCookies(&.{ "global2", "domain1" }, &matches); } { @@ -696,7 +713,7 @@ test "Jar: forRequest" { try std.Uri.parse("http://other.lightpanda.io/"), true, ); - try expectCookies(&.{}, &matches); + try expectCookies(&.{"global2"}, &matches); } { From 5dba5e85bb63008ae51af96d1110ba7cbbc1e009 Mon Sep 17 00:00:00 2001 From: katie-lpd Date: Sat, 1 Mar 2025 19:43:28 +0100 Subject: [PATCH 12/13] Update README.md A really important visual change in the readme :) --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9d9c3dd7d..5fa92e53b 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ Fast web automation for AI agents, LLM training, scraping and testing: - Ultra-low memory footprint (9x less than Chrome) - Exceptionally fast execution (11x faster than Chrome) - Instant startup -
-
[ ](https://github.com/lightpanda-io/demo) From 4b71c2671fd4b637ebdea781831fef4f5832f9df Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 7 Mar 2025 20:29:57 +0800 Subject: [PATCH 13/13] Serialize socket writes + consider client pending completions when shutting down Previously, we could have multiple in-flight messages from the server to a single client. This isn't safe and can lead to message interleaving. While write / send are atomic, they are only atomic for the N bytes which they write, which may not be the entire buffer. Consider this writeAll function: ``` pub fn writeAll(socket: socket_t, bytes: []const u8) !void { var index: usize = 0; while (index < bytes.len) { index += try posix.write(socket, bytes[index..]); } } ``` If we're trying to send "abc123", this could take anywhere from 1 to 6 calls to posix.write (it would take 6 calls, for example, if every call to posix.write only wrote a single byte). Now if you're trying to write other data to this same socket at the same time, messages _will_ get interleaved. In order for this to work, the client now has a send_queue (doubly linked list). When one message is sent, it sends the next. In addition to the above change, the Client is now self-contained with respect to its lifetime. This is necessary so that completions which come in AFTER our concept of its lifetime ends, can still be processed. I think all types that receive completions need to follow this model. This relies on the fact that kqueue (which I know for a fact) and io_uring (which people seem to imply) handle socket shutdown properly. It's still a bit messy because of timeout and not wanting to wait until timeout to accept new connections, but needing to wait until timeout to cleanup the client. The self-contained nature of Client makes it difficult to test as a generic. I removed Client(T). Tests now use real sockets. Some tests had to be removed because they're too difficult to test over a real connection :( --- src/server.zig | 1782 ++++++++++++++++++++---------------------------- 1 file changed, 751 insertions(+), 1031 deletions(-) diff --git a/src/server.zig b/src/server.zig index 926d7aaaa..cbb0b5007 100644 --- a/src/server.zig +++ b/src/server.zig @@ -47,37 +47,29 @@ const MAX_HTTP_REQUEST_SIZE = 2048; // +140 for the max control packet that might be interleaved in a message const MAX_MESSAGE_SIZE = 256 * 1024 + 14; -pub const Client = ClientT(*Server, CDP); - const Server = struct { allocator: Allocator, loop: *jsruntime.Loop, - current_client_id: usize = 0, // internal fields listener: posix.socket_t, - client: ?*Client = null, timeout: u64, - // a memory poor for our Send objects - send_pool: std.heap.MemoryPool(Send), - - // a memory poor for our Clietns + // A memory poor for our Clients. Because we only allow 1 client to be + // connected at a time, and because of the way we accept, we don't need + // a lock for this, since the creation of a new client cannot happen at the + // same time as the destruction of a client. client_pool: std.heap.MemoryPool(Client), - - completion_state_pool: std.heap.MemoryPool(CompletionState), + client_pool_lock: std.Thread.Mutex = .{}, // I/O fields - close_completion: Completion, accept_completion: Completion, // The response to send on a GET /json/version request json_version_response: []const u8, fn deinit(self: *Server) void { - self.send_pool.deinit(); self.client_pool.deinit(); - self.completion_state_pool.deinit(); self.allocator.free(self.json_version_response); } @@ -97,7 +89,6 @@ const Server = struct { completion: *Completion, result: AcceptError!posix.socket_t, ) void { - std.debug.assert(self.client == null); std.debug.assert(completion == &self.accept_completion); self.doCallbackAccept(result) catch |err| { log.err("accept error: {any}", .{err}); @@ -110,837 +101,836 @@ const Server = struct { result: AcceptError!posix.socket_t, ) !void { const socket = try result; - const client = try self.client_pool.create(); - errdefer self.client_pool.destroy(client); - self.current_client_id += 1; - client.* = Client.init(socket, self); + const client = blk: { + self.client_pool_lock.lock(); + defer self.client_pool_lock.unlock(); + break :blk try self.client_pool.create(); + }; - self.client = client; + errdefer { + self.client_pool_lock.lock(); + self.client_pool.destroy(client); + self.client_pool_lock.unlock(); + } + client.* = Client.init(socket, self); + client.start(); log.info("client connected", .{}); - try self.queueRead(); - try self.queueTimeout(); } - fn queueTimeout(self: *Server) !void { - const cs = try self.createCompletionState(); - self.loop.io.timeout( - *Server, - self, - callbackTimeout, - &cs.completion, - TimeoutCheck, - ); + fn releaseClient(self: *Server, client: *Client) void { + self.client_pool_lock.lock(); + self.client_pool.destroy(client); + self.client_pool_lock.unlock(); } +}; - fn callbackTimeout( - self: *Server, - completion: *Completion, - result: TimeoutError!void, - ) void { - const cs: *CompletionState = @alignCast( - @fieldParentPtr("completion", completion), - ); - defer self.completion_state_pool.destroy(cs); +// Client +// -------- + +pub const Client = struct { + // The client is initially serving HTTP requests but, under normal circumstances + // should eventually be upgraded to a websocket connections + mode: Mode, + + // The CDP instance that processes messages from this client + // (a generic so we can test with a mock + // null until mode == .websocket + cdp: ?CDP, + + // Our Server (a generic so we can test with a mock) + server: *Server, + reader: Reader(true), + socket: posix.socket_t, + last_active: std.time.Instant, + + // queue of messages to send + send_queue: SendQueue, + send_queue_node_pool: std.heap.MemoryPool(SendQueue.Node), + + read_pending: bool, + read_completion: Completion, + + write_pending: bool, + write_completion: Completion, + + timeout_pending: bool, + timeout_completion: Completion, - if (cs.client_id != self.current_client_id) { - // completion for a previously-connected client + // Used along with xyx_pending to figure out the lifetime of + // the client. When connected == false and we have no more pending + // completions, we can kill the client + connected: bool, + + const Mode = enum { + http, + websocket, + }; + + const EMPTY_PONG = [_]u8{ 138, 0 }; + + // CLOSE, 2 length, code + const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000 + const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009 + const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002 + // "private-use" close codes must be from 4000-49999 + const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000 + + const SendQueue = std.DoublyLinkedList(Outgoing); + + const Self = @This(); + + fn init(socket: posix.socket_t, server: *Server) Self { + return .{ + .cdp = null, + .mode = .http, + .socket = socket, + .server = server, + .last_active = now(), + .send_queue = .{}, + .read_pending = false, + .read_completion = undefined, + .write_pending = false, + .write_completion = undefined, + .timeout_pending = false, + .timeout_completion = undefined, + .connected = true, + .reader = .{ .allocator = server.allocator }, + .send_queue_node_pool = std.heap.MemoryPool(SendQueue.Node).init(server.allocator), + }; + } + + fn maybeDeinit(self: *Self) void { + if (self.read_pending or self.write_pending) { + // We cannot do anything as long as we still have these pending + // They should not be pending for long as we're only here after + // having shutdown the socket return; } - const client = self.client orelse return; + // We don't have a read nor a write completion pending, we can start + // to shutdown. - if (result) |_| { - if (now().since(client.last_active) > self.timeout) { - // close current connection - log.debug("conn timeout, closing...", .{}); - client.close(.timeout); - return; + self.reader.deinit(); + var node = self.send_queue.first; + while (node) |n| { + if (n.data.arena) |*arena| { + arena.deinit(); } - } else |err| { - log.err("timeout error: {any}", .{err}); + node = n.next; + } + if (self.cdp) |*cdp| { + cdp.deinit(); } + self.send_queue_node_pool.deinit(); + posix.close(self.socket); + + // let the client accept a new connection + self.server.queueAccept(); + + if (self.timeout_pending == false) { + // We also don't have a pending timeout, we can release the client. + // See callbackTimeout for more explanation about this. But, TL;DR + // we want to call `queueAccept` as soon as we have no more read/write + // but we don't want to wait for the timeout callback. + self.server.releaseClient(self); + } + } - // We re-queue this if the timeout hasn't been exceeded or on some - // very unlikely IO timeout error. - // AKA: we don't requeue this if the connection timed out and we - // closed the connection.s - self.queueTimeout() catch |err| { - log.err("queueTimeout error: {any}", .{err}); - }; + fn close(self: *Self) void { + self.connected = false; + // recv only, because we might have pending writes we'd like to get + // out (like the HTTP error response) + posix.shutdown(self.socket, .recv) catch {}; + self.maybeDeinit(); } - fn queueRead(self: *Server) !void { - var client = self.client orelse return; + fn start(self: *Self) void { + self.queueRead(); + self.queueTimeout(); + } - const cs = try self.createCompletionState(); - self.loop.io.recv( - *Server, + fn queueRead(self: *Self) void { + self.server.loop.io.recv( + *Self, self, callbackRead, - &cs.completion, - client.socket, - client.readBuf(), + &self.read_completion, + self.socket, + self.readBuf(), ); + self.read_pending = true; } - fn callbackRead( - self: *Server, - completion: *Completion, - result: RecvError!usize, - ) void { - const cs: *CompletionState = @alignCast( - @fieldParentPtr("completion", completion), - ); - defer self.completion_state_pool.destroy(cs); - - if (cs.client_id != self.current_client_id) { - // completion for a previously-connected client + fn callbackRead(self: *Self, _: *Completion, result: RecvError!usize) void { + self.read_pending = false; + if (self.connected == false) { + self.maybeDeinit(); return; } - var client = self.client orelse return; - const size = result catch |err| { log.err("read error: {any}", .{err}); - client.close(null); + self.close(); return; }; + if (size == 0) { - if (self.client != null) { - self.client = null; - } - self.queueAccept(); + self.close(); return; } - const more = client.processData(size) catch |err| { - log.err("Client Processing Error: {any}\n", .{err}); + const more = self.processData(size) catch { + self.close(); return; }; // if more == false, the client is disconnecting if (more) { - self.queueRead() catch |err| { - log.err("queueRead error: {any}", .{err}); - client.close(null); - }; + self.queueRead(); } } - fn queueSend( - self: *Server, - socket: posix.socket_t, - arena: ?ArenaAllocator, - data: []const u8, - ) !void { - const sd = try self.send_pool.create(); - errdefer self.send_pool.destroy(sd); - - const cs = try self.createCompletionState(); - errdefer self.completion_state_pool.destroy(cs); - - sd.* = .{ - .unsent = data, - .server = self, - .socket = socket, - .arena = arena, - .completion_state = cs, - }; - sd.queueSend(); - } - - fn queueClose(self: *Server, socket: posix.socket_t) void { - self.loop.io.close( - *Server, - self, - callbackClose, - &self.close_completion, - socket, - ); - var client = self.client.?; - client.deinit(); - self.client_pool.destroy(client); - self.client = null; - } - - fn callbackClose(self: *Server, completion: *Completion, _: CloseError!void) void { - std.debug.assert(completion == &self.close_completion); - self.queueAccept(); - } - - fn createCompletionState(self: *Server) !*CompletionState { - var cs = try self.completion_state_pool.create(); - cs.client_id = self.current_client_id; - cs.completion = undefined; - return cs; + fn readBuf(self: *Self) []u8 { + return self.reader.readBuf(); } -}; - -const CompletionState = struct { - client_id: usize, - completion: Completion, -}; - -// I/O Send -// -------- - -// NOTE: to allow concurrent send we create each time a dedicated context -// (with its own completion), allocated on the heap. -// After the send (on the sendCbk) the dedicated context will be destroy -// and the data slice will be free. -const Send = struct { // Any unsent data we have. - unsent: []const u8, - - server: *Server, - socket: posix.socket_t, - completion_state: *CompletionState, - // If we need to free anything when we're done - arena: ?ArenaAllocator, + fn processData(self: *Self, len: usize) !bool { + self.last_active = now(); + self.reader.len += len; - fn deinit(self: *Send) void { - if (self.arena) |arena| { - arena.deinit(); + switch (self.mode) { + .http => { + try self.processHTTPRequest(); + return true; + }, + .websocket => return self.processWebsocketMessage(), } - - var server = self.server; - server.completion_state_pool.destroy(self.completion_state); - server.send_pool.destroy(self); } - fn queueSend(self: *Send) void { - self.server.loop.io.send( - *Send, - self, - sendCallback, - &self.completion_state.completion, - self.socket, - self.unsent, - ); - } + fn processHTTPRequest(self: *Self) !void { + std.debug.assert(self.reader.pos == 0); + const request = self.reader.buf[0..self.reader.len]; - fn sendCallback(self: *Send, _: *Completion, result: SendError!usize) void { - const server = self.server; - const cs = self.completion_state; + if (request.len > MAX_HTTP_REQUEST_SIZE) { + self.writeHTTPErrorResponse(413, "Request too large"); + return error.RequestTooLarge; + } - if (cs.client_id != server.current_client_id) { - // completion for a previously-connected client - self.deinit(); + // we're only expecting [body-less] GET requests. + if (std.mem.endsWith(u8, request, "\r\n\r\n") == false) { + // we need more data, put any more data here return; } - const sent = result catch |err| { - log.info("send error: {any}", .{err}); - if (server.client) |client| { - client.close(null); + self.handleHTTPRequest(request) catch |err| { + switch (err) { + error.NotFound => self.writeHTTPErrorResponse(404, "Not found"), + error.InvalidRequest => self.writeHTTPErrorResponse(400, "Invalid request"), + error.InvalidProtocol => self.writeHTTPErrorResponse(400, "Invalid HTTP protocol"), + error.MissingHeaders => self.writeHTTPErrorResponse(400, "Missing required header"), + error.InvalidUpgradeHeader => self.writeHTTPErrorResponse(400, "Unsupported upgrade type"), + error.InvalidVersionHeader => self.writeHTTPErrorResponse(400, "Invalid websocket version"), + error.InvalidConnectionHeader => self.writeHTTPErrorResponse(400, "Invalid connection header"), + else => { + log.err("error processing HTTP request: {any}", .{err}); + self.writeHTTPErrorResponse(500, "Internal Server Error"); + }, } - self.deinit(); - return; + return err; }; - if (sent == self.unsent.len) { - self.deinit(); - return; - } - - // partial send, re-queue a send for whatever we have left - self.unsent = self.unsent[sent..]; - self.queueSend(); + // the next incoming data can go to the front of our buffer + self.reader.len = 0; } -}; - -// Client -// -------- -// This is a generic only so that it can be unit tested. Normally, S == Server -// and when we send a message, we'll use server.send(...) to send via the server's -// IO loop. During tests, we can inject a simple mock to record (and then verify) -// the send message -fn ClientT(comptime S: type, comptime C: type) type { - const EMPTY_PONG = [_]u8{ 138, 0 }; + fn handleHTTPRequest(self: *Self, request: []u8) !void { + if (request.len < 18) { + // 18 is [generously] the smallest acceptable HTTP request + return error.InvalidRequest; + } - // CLOSE, 2 length, code - const CLOSE_NORMAL = [_]u8{ 136, 2, 3, 232 }; // code: 1000 - const CLOSE_TOO_BIG = [_]u8{ 136, 2, 3, 241 }; // 1009 - const CLOSE_PROTOCOL_ERROR = [_]u8{ 136, 2, 3, 234 }; //code: 1002 - // "private-use" close codes must be from 4000-49999 - const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000 + if (std.mem.eql(u8, request[0..4], "GET ") == false) { + return error.NotFound; + } - return struct { - // The client is initially serving HTTP requests but, under normal circumstances - // should eventually be upgraded to a websocket connections - mode: Mode, - - // The CDP instance that processes messages from this client - // (a generic so we can test with a mock - // null until mode == .websocket - cdp: ?C, - - // Our Server (a generic so we can test with a mock) - server: S, - reader: Reader, - socket: posix.socket_t, - last_active: std.time.Instant, - - const Mode = enum { - http, - websocket, + const url_end = std.mem.indexOfScalarPos(u8, request, 4, ' ') orelse { + return error.InvalidRequest; }; - const Self = @This(); + const url = request[4..url_end]; - fn init(socket: posix.socket_t, server: S) Self { - return .{ - .cdp = null, - .mode = .http, - .socket = socket, - .server = server, - .last_active = now(), - .reader = .{ .allocator = server.allocator }, - }; + if (std.mem.eql(u8, url, "/")) { + return self.upgradeConnection(request); } - pub fn deinit(self: *Self) void { - self.reader.deinit(); - if (self.cdp) |*cdp| { - cdp.deinit(); - } + if (std.mem.eql(u8, url, "/json/version")) { + return self.send(null, self.server.json_version_response); } - pub fn close(self: *Self, close_code: ?CloseCode) void { - if (close_code) |code| { - if (self.mode == .websocket) { - switch (code) { - .timeout => self.send(&CLOSE_TIMEOUT) catch {}, - } - } - } - self.server.queueClose(self.socket); - } + return error.NotFound; + } - fn readBuf(self: *Self) []u8 { - return self.reader.readBuf(); + fn upgradeConnection(self: *Self, request: []u8) !void { + // our caller already confirmed that we have a trailing \r\n\r\n + const request_line_end = std.mem.indexOfScalar(u8, request, '\r') orelse unreachable; + const request_line = request[0..request_line_end]; + + if (!std.ascii.endsWithIgnoreCase(request_line, "http/1.1")) { + return error.InvalidProtocol; } - fn processData(self: *Self, len: usize) !bool { - self.last_active = now(); - self.reader.len += len; + // we need to extract the sec-websocket-key value + var key: []const u8 = ""; - switch (self.mode) { - .http => { - try self.processHTTPRequest(); - return true; - }, - .websocket => return self.processWebsocketMessage(), - } - } + // we need to make sure that we got all the necessary headers + values + var required_headers: u8 = 0; - fn processHTTPRequest(self: *Self) !void { - std.debug.assert(self.reader.pos == 0); - const request = self.reader.buf[0..self.reader.len]; + // can't std.mem.split because it forces the iterated value to be const + // (we could @constCast...) - errdefer self.server.queueClose(self.socket); + var buf = request[request_line_end + 2 ..]; - if (request.len > MAX_HTTP_REQUEST_SIZE) { - self.writeHTTPErrorResponse(413, "Request too large"); - return error.RequestTooLarge; - } + while (buf.len > 4) { + const index = std.mem.indexOfScalar(u8, buf, '\r') orelse unreachable; + const separator = std.mem.indexOfScalar(u8, buf[0..index], ':') orelse return error.InvalidRequest; - // we're only expecting [body-less] GET requests. - if (std.mem.endsWith(u8, request, "\r\n\r\n") == false) { - // we need more data, put any more data here - return; - } + const name = std.mem.trim(u8, toLower(buf[0..separator]), &std.ascii.whitespace); + const value = std.mem.trim(u8, buf[(separator + 1)..index], &std.ascii.whitespace); - self.handleHTTPRequest(request) catch |err| { - switch (err) { - error.NotFound => self.writeHTTPErrorResponse(404, "Not found"), - error.InvalidRequest => self.writeHTTPErrorResponse(400, "Invalid request"), - error.InvalidProtocol => self.writeHTTPErrorResponse(400, "Invalid HTTP protocol"), - error.MissingHeaders => self.writeHTTPErrorResponse(400, "Missing required header"), - error.InvalidUpgradeHeader => self.writeHTTPErrorResponse(400, "Unsupported upgrade type"), - error.InvalidVersionHeader => self.writeHTTPErrorResponse(400, "Invalid websocket version"), - error.InvalidConnectionHeader => self.writeHTTPErrorResponse(400, "Invalid connection header"), - else => { - log.err("error processing HTTP request: {any}", .{err}); - self.writeHTTPErrorResponse(500, "Internal Server Error"); - }, + if (std.mem.eql(u8, name, "upgrade")) { + if (!std.ascii.eqlIgnoreCase("websocket", value)) { + return error.InvalidUpgradeHeader; } - return err; - }; + required_headers |= 1; + } else if (std.mem.eql(u8, name, "sec-websocket-version")) { + if (value.len != 2 or value[0] != '1' or value[1] != '3') { + return error.InvalidVersionHeader; + } + required_headers |= 2; + } else if (std.mem.eql(u8, name, "connection")) { + // find if connection header has upgrade in it, example header: + // Connection: keep-alive, Upgrade + if (std.ascii.indexOfIgnoreCase(value, "upgrade") == null) { + return error.InvalidConnectionHeader; + } + required_headers |= 4; + } else if (std.mem.eql(u8, name, "sec-websocket-key")) { + key = value; + required_headers |= 8; + } - // the next incoming data can go to the front of our buffer - self.reader.len = 0; + const next = index + 2; + buf = buf[next..]; } - fn handleHTTPRequest(self: *Self, request: []u8) !void { - if (request.len < 18) { - // 18 is [generously] the smallest acceptable HTTP request - return error.InvalidRequest; - } - - if (std.mem.eql(u8, request[0..4], "GET ") == false) { - return error.NotFound; - } + if (required_headers != 15) { + return error.MissingHeaders; + } - const url_end = std.mem.indexOfScalarPos(u8, request, 4, ' ') orelse { - return error.InvalidRequest; - }; + // our caller has already made sure this request ended in \r\n\r\n + // so it isn't something we need to check again - const url = request[4..url_end]; + var arena = ArenaAllocator.init(self.server.allocator); + errdefer arena.deinit(); - if (std.mem.eql(u8, url, "/")) { - return self.upgradeConnection(request); - } + const response = blk: { + // Response to an ugprade request is always this, with + // the Sec-Websocket-Accept value a spacial sha1 hash of the + // request "sec-websocket-version" and a magic value. - if (std.mem.eql(u8, url, "/json/version")) { - return self.send(self.server.json_version_response); - } + const template = + "HTTP/1.1 101 Switching Protocols\r\n" ++ + "Upgrade: websocket\r\n" ++ + "Connection: upgrade\r\n" ++ + "Sec-Websocket-Accept: 0000000000000000000000000000\r\n\r\n"; - return error.NotFound; - } + // The response will be sent via the IO Loop and thus has to have its + // own lifetime. + const res = try arena.allocator().dupe(u8, template); - fn upgradeConnection(self: *Self, request: []u8) !void { - // our caller already confirmed that we have a trailing \r\n\r\n - const request_line_end = std.mem.indexOfScalar(u8, request, '\r') orelse unreachable; - const request_line = request[0..request_line_end]; + // magic response + const key_pos = res.len - 32; + var h: [20]u8 = undefined; + var hasher = std.crypto.hash.Sha1.init(.{}); + hasher.update(key); + // websocket spec always used this value + hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + hasher.final(&h); - if (!std.ascii.endsWithIgnoreCase(request_line, "http/1.1")) { - return error.InvalidProtocol; - } + _ = std.base64.standard.Encoder.encode(res[key_pos .. key_pos + 28], h[0..]); - // we need to extract the sec-websocket-key value - var key: []const u8 = ""; + break :blk res; + }; - // we need to make sure that we got all the necessary headers + values - var required_headers: u8 = 0; + self.mode = .websocket; + self.cdp = CDP.init(self.server.allocator, self, self.server.loop); + return self.send(arena, response); + } - // can't std.mem.split because it forces the iterated value to be const - // (we could @constCast...) + fn writeHTTPErrorResponse(self: *Self, comptime status: u16, comptime body: []const u8) void { + const response = std.fmt.comptimePrint( + "HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}", + .{ status, body.len, body }, + ); - var buf = request[request_line_end + 2 ..]; + // we're going to close this connection anyways, swallowing any + // error seems safe + self.send(null, response) catch {}; + } - while (buf.len > 4) { - const index = std.mem.indexOfScalar(u8, buf, '\r') orelse unreachable; - const separator = std.mem.indexOfScalar(u8, buf[0..index], ':') orelse return error.InvalidRequest; + fn processWebsocketMessage(self: *Self) !bool { + errdefer self.close(); - const name = std.mem.trim(u8, toLower(buf[0..separator]), &std.ascii.whitespace); - const value = std.mem.trim(u8, buf[(separator + 1)..index], &std.ascii.whitespace); + var reader = &self.reader; - if (std.mem.eql(u8, name, "upgrade")) { - if (!std.ascii.eqlIgnoreCase("websocket", value)) { - return error.InvalidUpgradeHeader; - } - required_headers |= 1; - } else if (std.mem.eql(u8, name, "sec-websocket-version")) { - if (value.len != 2 or value[0] != '1' or value[1] != '3') { - return error.InvalidVersionHeader; - } - required_headers |= 2; - } else if (std.mem.eql(u8, name, "connection")) { - // find if connection header has upgrade in it, example header: - // Connection: keep-alive, Upgrade - if (std.ascii.indexOfIgnoreCase(value, "upgrade") == null) { - return error.InvalidConnectionHeader; - } - required_headers |= 4; - } else if (std.mem.eql(u8, name, "sec-websocket-key")) { - key = value; - required_headers |= 8; + while (true) { + const msg = reader.next() catch |err| { + switch (err) { + error.TooLarge => self.send(null, &CLOSE_TOO_BIG) catch {}, + error.NotMasked => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {}, + error.ReservedFlags => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {}, + error.InvalidMessageType => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {}, + error.ControlTooLarge => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {}, + error.InvalidContinuation => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {}, + error.NestedFragementation => self.send(null, &CLOSE_PROTOCOL_ERROR) catch {}, + error.OutOfMemory => {}, // don't borther trying to send an error in this case } - - const next = index + 2; - buf = buf[next..]; + return err; + } orelse break; + + switch (msg.type) { + .pong => {}, + .ping => try self.sendPong(msg.data), + .close => { + self.send(null, &CLOSE_NORMAL) catch {}; + self.close(); + return false; + }, + .text, .binary => if (self.cdp.?.handleMessage(msg.data) == false) { + self.close(); + return false; + }, } - - if (required_headers != 15) { - return error.MissingHeaders; + if (msg.cleanup_fragment) { + reader.cleanup(); } + } - // our caller has already made sure this request ended in \r\n\r\n - // so it isn't something we need to check again + // We might have read part of the next message. Our reader potentially + // has to move data around in its buffer to make space. + reader.compact(); + return true; + } - var arena = ArenaAllocator.init(self.server.allocator); - errdefer arena.deinit(); + fn sendPong(self: *Self, data: []const u8) !void { + if (data.len == 0) { + return self.send(null, &EMPTY_PONG); + } + var header_buf: [10]u8 = undefined; + const header = websocketHeader(&header_buf, .pong, data.len); - const response = blk: { - // Response to an ugprade request is always this, with - // the Sec-Websocket-Accept value a spacial sha1 hash of the - // request "sec-websocket-version" and a magic value. + var arena = ArenaAllocator.init(self.server.allocator); + errdefer arena.deinit(); - const template = - "HTTP/1.1 101 Switching Protocols\r\n" ++ - "Upgrade: websocket\r\n" ++ - "Connection: upgrade\r\n" ++ - "Sec-Websocket-Accept: 0000000000000000000000000000\r\n\r\n"; + var framed = try arena.allocator().alloc(u8, header.len + data.len); + @memcpy(framed[0..header.len], header); + @memcpy(framed[header.len..], data); + return self.send(arena, framed); + } - // The response will be sent via the IO Loop and thus has to have its - // own lifetime. - const res = try arena.allocator().dupe(u8, template); + // called by CDP + // Websocket frames have a variable lenght header. For server-client, + // it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have + // writev, so we need to get creative. We'll JSON serialize to a + // buffer, where the first 10 bytes are reserved. We can then backfill + // the header and send the slice. + pub fn sendJSON(self: *Self, message: anytype, opts: std.json.StringifyOptions) !void { + var arena = ArenaAllocator.init(self.server.allocator); + errdefer arena.deinit(); - // magic response - const key_pos = res.len - 32; - var h: [20]u8 = undefined; - var hasher = std.crypto.hash.Sha1.init(.{}); - hasher.update(key); - // websocket spec always used this value - hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); - hasher.final(&h); + const allocator = arena.allocator(); - _ = std.base64.standard.Encoder.encode(res[key_pos .. key_pos + 28], h[0..]); + var buf: std.ArrayListUnmanaged(u8) = .{}; + try buf.ensureTotalCapacity(allocator, 512); - break :blk res; - }; + // reserve space for the maximum possible header + buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); - self.mode = .websocket; - self.cdp = C.init(self.server.allocator, self, self.server.loop); - return self.sendAlloc(arena, response); - } + try std.json.stringify(message, opts, buf.writer(allocator)); + const framed = fillWebsocketHeader(buf); + return self.send(arena, framed); + } - fn writeHTTPErrorResponse(self: *Self, comptime status: u16, comptime body: []const u8) void { - const response = std.fmt.comptimePrint( - "HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}", - .{ status, body.len, body }, - ); + pub fn sendJSONRaw( + self: *Self, + arena: ArenaAllocator, + buf: std.ArrayListUnmanaged(u8), + ) !void { + // Dangerous API!. We assume the caller has reserved the first 10 + // bytes in `buf`. + const framed = fillWebsocketHeader(buf); + return self.send(arena, framed); + } - // we're going to close this connection anyways, swallowing any - // error seems safe - self.send(response) catch {}; - } + fn queueTimeout(self: *Self) void { + self.server.loop.io.timeout( + *Self, + self, + callbackTimeout, + &self.timeout_completion, + TimeoutCheck, + ); + self.timeout_pending = true; + } - fn processWebsocketMessage(self: *Self) !bool { - errdefer self.server.queueClose(self.socket); - - var reader = &self.reader; - - while (true) { - const msg = reader.next() catch |err| { - switch (err) { - error.TooLarge => self.send(&CLOSE_TOO_BIG) catch {}, - error.NotMasked => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, - error.ReservedFlags => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, - error.InvalidMessageType => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, - error.ControlTooLarge => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, - error.InvalidContinuation => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, - error.NestedFragementation => self.send(&CLOSE_PROTOCOL_ERROR) catch {}, - error.OutOfMemory => {}, // don't borther trying to send an error in this case - } - return err; - } orelse break; - - switch (msg.type) { - .pong => {}, - .ping => try self.sendPong(msg.data), - .close => { - self.send(&CLOSE_NORMAL) catch {}; - self.server.queueClose(self.socket); - return false; - }, - .text, .binary => if (self.cdp.?.handleMessage(msg.data) == false) { - self.close(null); - return false; - }, - } - if (msg.cleanup_fragment) { - reader.cleanup(); - } + fn callbackTimeout(self: *Self, _: *Completion, result: TimeoutError!void) void { + self.timeout_pending = false; + if (self.connected == false) { + if (self.read_pending == false and self.write_pending == false) { + // Timeout is problematic. Ideally, we'd just call maybeDeinit + // here and check for timeout_pending == true. But that would + // mean not being able to accept a new connection until this + // callback fires - introducing a noticeable delay. + // So, when read_pending and write_pending are both false, we + // clean up as much as we can, and let the server accept a new + // connection but we keep the client around to handle this + // completion (if only we could cancel a completion!). + // If we're here, with connected == false, read_pending == false + // and write_pending == false, then everything has already been + // cleaned up, and we just need to release the client. + self.server.releaseClient(self); } - - // We might have read part of the next message. Our reader potentially - // has to move data around in its buffer to make space. - reader.compact(); - return true; + return; } - fn sendPong(self: *Self, data: []const u8) !void { - if (data.len == 0) { - return self.send(&EMPTY_PONG); + if (result) |_| { + if (now().since(self.last_active) >= self.server.timeout) { + if (self.mode == .websocket) { + self.send(null, &CLOSE_TIMEOUT) catch {}; + } + self.close(); + return; } - var header_buf: [10]u8 = undefined; - const header = websocketHeader(&header_buf, .pong, data.len); + } else |err| { + log.err("timeout error: {any}", .{err}); + } - var arena = ArenaAllocator.init(self.server.allocator); - errdefer arena.deinit(); + self.queueTimeout(); + } - var framed = try arena.allocator().alloc(u8, header.len + data.len); - @memcpy(framed[0..header.len], header); - @memcpy(framed[header.len..], data); - return self.sendAlloc(arena, framed); - } + fn send(self: *Self, arena: ?ArenaAllocator, data: []const u8) !void { + const node = try self.send_queue_node_pool.create(); + errdefer self.send_queue_node_pool.destroy(node); - // called by CDP - // Websocket frames have a variable lenght header. For server-client, - // it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have - // writev, so we need to get creative. We'll JSON serialize to a - // buffer, where the first 10 bytes are reserved. We can then backfill - // the header and send the slice. - pub fn sendJSON(self: *Self, message: anytype, opts: std.json.StringifyOptions) !void { - var arena = ArenaAllocator.init(self.server.allocator); - errdefer arena.deinit(); + node.data = Outgoing{ + .arena = arena, + .to_send = data, + }; + self.send_queue.append(node); - const allocator = arena.allocator(); + if (self.send_queue.len > 1) { + // if we already had a message in the queue, then our send loop + // is already setup. + return; + } + self.queueSend(); + } - var buf: std.ArrayListUnmanaged(u8) = .{}; - try buf.ensureTotalCapacity(allocator, 512); + fn queueSend(self: *Self) void { + const node = self.send_queue.first orelse { + // no more messages to send; + return; + }; - // reserve space for the maximum possible header - buf.appendSliceAssumeCapacity(&.{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); + self.server.loop.io.send( + *Self, + self, + sendCallback, + &self.write_completion, + self.socket, + node.data.to_send, + ); + self.write_pending = true; + } - try std.json.stringify(message, opts, buf.writer(allocator)); - const framed = fillWebsocketHeader(buf); - return self.sendAlloc(arena, framed); + fn sendCallback(self: *Self, _: *Completion, result: SendError!usize) void { + self.write_pending = false; + if (self.connected == false) { + self.maybeDeinit(); + return; } - pub fn sendJSONRaw( - self: *Self, - arena: ArenaAllocator, - buf: std.ArrayListUnmanaged(u8), - ) !void { - // Dangerous API!. We assume the caller has reserved the first 10 - // bytes in `buf`. - const framed = fillWebsocketHeader(buf); - return self.sendAlloc(arena, framed); - } + const sent = result catch |err| { + log.info("send error: {any}", .{err}); + self.close(); + return; + }; - fn send(self: *Self, data: []const u8) !void { - return self.server.queueSend(self.socket, null, data); + const node = self.send_queue.popFirst().?; + const outgoing = &node.data; + if (sent == outgoing.to_send.len) { + if (outgoing.arena) |*arena| { + arena.deinit(); + } + self.send_queue_node_pool.destroy(node); + } else { + // oops, we shouldn't have popped this node off, we need + // to add it back to the front in order to send the unsent data + // (this is less likely to happen, which is why we eagerly + // pop it off) + std.debug.assert(sent < outgoing.to_send.len); + node.data.to_send = outgoing.to_send[sent..]; + self.send_queue.prepend(node); } + self.queueSend(); + } +}; - fn sendAlloc(self: *Self, arena: ArenaAllocator, data: []const u8) !void { - return self.server.queueSend(self.socket, arena, data); - } - }; -} +const Outgoing = struct { + to_send: []const u8, + arena: ?ArenaAllocator, +}; // WebSocket message reader. Given websocket message, acts as an iterator that // can return zero or more Messages. When next returns null, any incomplete // message will remain in reader.data -const Reader = struct { - allocator: Allocator, +fn Reader(comptime EXPECT_MASK: bool) type { + return struct { + allocator: Allocator, - // position in buf of the start of the next message - pos: usize = 0, + // position in buf of the start of the next message + pos: usize = 0, - // position in buf up until where we have valid data - // (any new reads must be placed after this) - len: usize = 0, + // position in buf up until where we have valid data + // (any new reads must be placed after this) + len: usize = 0, - // we add 140 to allow 1 control message (ping/pong/close) to be - // fragmented into a normal message. - buf: [MAX_MESSAGE_SIZE + 140]u8 = undefined, + // we add 140 to allow 1 control message (ping/pong/close) to be + // fragmented into a normal message. + buf: [MAX_MESSAGE_SIZE + 140]u8 = undefined, - fragments: ?Fragments = null, + fragments: ?Fragments = null, - fn deinit(self: *Reader) void { - self.cleanup(); - } + const Self = @This(); - fn cleanup(self: *Reader) void { - if (self.fragments) |*f| { - f.message.deinit(self.allocator); - self.fragments = null; + fn deinit(self: *Self) void { + self.cleanup(); } - } - fn readBuf(self: *Reader) []u8 { - // We might have read a partial http or websocket message. - // Subsequent reads must read from where we left off. - return self.buf[self.len..]; - } + fn cleanup(self: *Self) void { + if (self.fragments) |*f| { + f.message.deinit(self.allocator); + self.fragments = null; + } + } - fn next(self: *Reader) !?Message { - LOOP: while (true) { - var buf = self.buf[self.pos..self.len]; + fn readBuf(self: *Self) []u8 { + // We might have read a partial http or websocket message. + // Subsequent reads must read from where we left off. + return self.buf[self.len..]; + } - const length_of_len, const message_len = extractLengths(buf) orelse { - // we don't have enough bytes - return null; - }; + fn next(self: *Self) !?Message { + LOOP: while (true) { + var buf = self.buf[self.pos..self.len]; - const byte1 = buf[0]; + const length_of_len, const message_len = extractLengths(buf) orelse { + // we don't have enough bytes + return null; + }; - if (byte1 & 112 != 0) { - return error.ReservedFlags; - } + const byte1 = buf[0]; - if (buf[1] & 128 != 128) { - // client -> server messages _must_ be masked - return error.NotMasked; - } + if (byte1 & 112 != 0) { + return error.ReservedFlags; + } - var is_control = false; - var is_continuation = false; - var message_type: Message.Type = undefined; - switch (byte1 & 15) { - 0 => is_continuation = true, - 1 => message_type = .text, - 2 => message_type = .binary, - 8 => { - is_control = true; - message_type = .close; - }, - 9 => { - is_control = true; - message_type = .ping; - }, - 10 => { - is_control = true; - message_type = .pong; - }, - else => return error.InvalidMessageType, - } + if (comptime EXPECT_MASK) { + if (buf[1] & 128 != 128) { + // client -> server messages _must_ be masked + return error.NotMasked; + } + } else if (buf[1] & 128 != 0) { + // server -> client are never masked + return error.Masked; + } + + var is_control = false; + var is_continuation = false; + var message_type: Message.Type = undefined; + switch (byte1 & 15) { + 0 => is_continuation = true, + 1 => message_type = .text, + 2 => message_type = .binary, + 8 => { + is_control = true; + message_type = .close; + }, + 9 => { + is_control = true; + message_type = .ping; + }, + 10 => { + is_control = true; + message_type = .pong; + }, + else => return error.InvalidMessageType, + } - if (is_control) { - if (message_len > 125) { - return error.ControlTooLarge; + if (is_control) { + if (message_len > 125) { + return error.ControlTooLarge; + } + } else if (message_len > MAX_MESSAGE_SIZE) { + return error.TooLarge; } - } else if (message_len > MAX_MESSAGE_SIZE) { - return error.TooLarge; - } - if (buf.len < message_len) { - return null; - } + if (buf.len < message_len) { + return null; + } - // prefix + length_of_len + mask - const header_len = 2 + length_of_len + 4; + // prefix + length_of_len + mask + const header_len = 2 + length_of_len + if (comptime EXPECT_MASK) 4 else 0; - const payload = buf[header_len..message_len]; - mask(buf[header_len - 4 .. header_len], payload); + const payload = buf[header_len..message_len]; + if (comptime EXPECT_MASK) { + mask(buf[header_len - 4 .. header_len], payload); + } - // whatever happens after this, we know where the next message starts - self.pos += message_len; + // whatever happens after this, we know where the next message starts + self.pos += message_len; - const fin = byte1 & 128 == 128; + const fin = byte1 & 128 == 128; - if (is_continuation) { - const fragments = &(self.fragments orelse return error.InvalidContinuation); - if (fragments.message.items.len + message_len > MAX_MESSAGE_SIZE) { - return error.TooLarge; + if (is_continuation) { + const fragments = &(self.fragments orelse return error.InvalidContinuation); + if (fragments.message.items.len + message_len > MAX_MESSAGE_SIZE) { + return error.TooLarge; + } + + try fragments.message.appendSlice(self.allocator, payload); + + if (fin == false) { + // maybe we have more parts of the message waiting + continue :LOOP; + } + + // this continuation is done! + return .{ + .type = fragments.type, + .data = fragments.message.items, + .cleanup_fragment = true, + }; } - try fragments.message.appendSlice(self.allocator, payload); + const can_be_fragmented = message_type == .text or message_type == .binary; + if (self.fragments != null and can_be_fragmented) { + // if this isn't a continuation, then we can't have fragments + return error.NestedFragementation; + } if (fin == false) { - // maybe we have more parts of the message waiting + if (can_be_fragmented == false) { + return error.InvalidContinuation; + } + + // not continuation, and not fin. It has to be the first message + // in a fragmented message. + var fragments = Fragments{ .message = .{}, .type = message_type }; + try fragments.message.appendSlice(self.allocator, payload); + self.fragments = fragments; continue :LOOP; } - // this continuation is done! return .{ - .type = fragments.type, - .data = fragments.message.items, - .cleanup_fragment = true, + .data = payload, + .type = message_type, + .cleanup_fragment = false, }; } + } - const can_be_fragmented = message_type == .text or message_type == .binary; - if (self.fragments != null and can_be_fragmented) { - // if this isn't a continuation, then we can't have fragements - return error.NestedFragementation; - } - - if (fin == false) { - if (can_be_fragmented == false) { - return error.InvalidContinuation; - } - - // not continuation, and not fin. It has to be the first message - // in a fragemented message. - var fragments = Fragments{ .message = .{}, .type = message_type }; - try fragments.message.appendSlice(self.allocator, payload); - self.fragments = fragments; - continue :LOOP; + fn extractLengths(buf: []const u8) ?struct { usize, usize } { + if (buf.len < 2) { + return null; } - return .{ - .data = payload, - .type = message_type, - .cleanup_fragment = false, + const length_of_len: usize = switch (buf[1] & 127) { + 126 => 2, + 127 => 8, + else => 0, }; - } - } - fn extractLengths(buf: []const u8) ?struct { usize, usize } { - if (buf.len < 2) { - return null; - } + if (buf.len < length_of_len + 2) { + // we definitely don't have enough buf yet + return null; + } - const length_of_len: usize = switch (buf[1] & 127) { - 126 => 2, - 127 => 8, - else => 0, - }; + const message_len = switch (length_of_len) { + 2 => @as(u16, @intCast(buf[3])) | @as(u16, @intCast(buf[2])) << 8, + 8 => @as(u64, @intCast(buf[9])) | @as(u64, @intCast(buf[8])) << 8 | @as(u64, @intCast(buf[7])) << 16 | @as(u64, @intCast(buf[6])) << 24 | @as(u64, @intCast(buf[5])) << 32 | @as(u64, @intCast(buf[4])) << 40 | @as(u64, @intCast(buf[3])) << 48 | @as(u64, @intCast(buf[2])) << 56, + else => buf[1] & 127, + } + length_of_len + 2 + if (comptime EXPECT_MASK) 4 else 0; // +2 for header prefix, +4 for mask; - if (buf.len < length_of_len + 2) { - // we definitely don't have enough buf yet - return null; + return .{ length_of_len, message_len }; } - const message_len = switch (length_of_len) { - 2 => @as(u16, @intCast(buf[3])) | @as(u16, @intCast(buf[2])) << 8, - 8 => @as(u64, @intCast(buf[9])) | @as(u64, @intCast(buf[8])) << 8 | @as(u64, @intCast(buf[7])) << 16 | @as(u64, @intCast(buf[6])) << 24 | @as(u64, @intCast(buf[5])) << 32 | @as(u64, @intCast(buf[4])) << 40 | @as(u64, @intCast(buf[3])) << 48 | @as(u64, @intCast(buf[2])) << 56, - else => buf[1] & 127, - } + length_of_len + 2 + 4; // +2 for header prefix, +4 for mask; - - return .{ length_of_len, message_len }; - } - - // This is called after we've processed complete websocket messages (this - // only applies to websocket messages). - // There are three cases: - // 1 - We don't have any incomplete data (for a subsequent message) in buf. - // This is the easier to handle, we can set pos & len to 0. - // 2 - We have part of the next message, but we know it'll fit in the - // remaining buf. We don't need to do anything - // 3 - We have part of the next message, but either it won't fight into the - // remaining buffer, or we don't know (because we don't have enough - // of the header to tell the length). We need to "compact" the buffer - fn compact(self: *Reader) void { - const pos = self.pos; - const len = self.len; - - std.debug.assert(pos <= len); - - // how many (if any) partial bytes do we have - const partial_bytes = len - pos; - - if (partial_bytes == 0) { - // We have no partial bytes. Setting these to 0 ensures that we - // get the best utilization of our buffer - self.pos = 0; - self.len = 0; - return; - } + // This is called after we've processed complete websocket messages (this + // only applies to websocket messages). + // There are three cases: + // 1 - We don't have any incomplete data (for a subsequent message) in buf. + // This is the easier to handle, we can set pos & len to 0. + // 2 - We have part of the next message, but we know it'll fit in the + // remaining buf. We don't need to do anything + // 3 - We have part of the next message, but either it won't fight into the + // remaining buffer, or we don't know (because we don't have enough + // of the header to tell the length). We need to "compact" the buffer + fn compact(self: *Self) void { + const pos = self.pos; + const len = self.len; + + std.debug.assert(pos <= len); + + // how many (if any) partial bytes do we have + const partial_bytes = len - pos; + + if (partial_bytes == 0) { + // We have no partial bytes. Setting these to 0 ensures that we + // get the best utilization of our buffer + self.pos = 0; + self.len = 0; + return; + } - const partial = self.buf[pos..len]; + const partial = self.buf[pos..len]; - // If we have enough bytes of the next message to tell its length - // we'll be able to figure out whether we need to do anything or not. - if (extractLengths(partial)) |length_meta| { - const next_message_len = length_meta.@"1"; - // if this isn't true, then we have a full message and it - // should have been processed. - std.debug.assert(next_message_len > partial_bytes); + // If we have enough bytes of the next message to tell its length + // we'll be able to figure out whether we need to do anything or not. + if (extractLengths(partial)) |length_meta| { + const next_message_len = length_meta.@"1"; + // if this isn't true, then we have a full message and it + // should have been processed. + std.debug.assert(next_message_len > partial_bytes); - const missing_bytes = next_message_len - partial_bytes; + const missing_bytes = next_message_len - partial_bytes; - const free_space = self.buf.len - len; - if (missing_bytes < free_space) { - // we have enough space in our buffer, as is, - return; + const free_space = self.buf.len - len; + if (missing_bytes < free_space) { + // we have enough space in our buffer, as is, + return; + } } - } - // We're here because we either don't have enough bytes of the next - // message, or we know that it won't fit in our buffer as-is. - std.mem.copyForwards(u8, &self.buf, partial); - self.pos = 0; - self.len = partial_bytes; - } -}; + // We're here because we either don't have enough bytes of the next + // message, or we know that it won't fit in our buffer as-is. + std.mem.copyForwards(u8, &self.buf, partial); + self.pos = 0; + self.len = partial_bytes; + } + }; +} const Fragments = struct { type: Message.Type, @@ -1057,12 +1047,9 @@ pub fn run( .timeout = timeout, .listener = listener, .allocator = allocator, - .close_completion = undefined, .accept_completion = undefined, .json_version_response = json_version_response, - .send_pool = std.heap.MemoryPool(Send).init(allocator), .client_pool = std.heap.MemoryPool(Client).init(allocator), - .completion_state_pool = std.heap.MemoryPool(CompletionState).init(allocator), }; defer server.deinit(); @@ -1158,66 +1145,60 @@ test "server: buildJSONVersionResponse" { } test "Client: http invalid request" { - try assertHTTPError( - error.RequestTooLarge, - 413, - "Request too large", - "GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 2050) ++ "\r\n\r\n", - ); + var c = try createTestClient(); + defer c.deinit(); + + const res = try c.httpRequest("GET /over/9000 HTTP/1.1\r\n" ++ "Header: " ++ ("a" ** 2050) ++ "\r\n\r\n"); + try testing.expectEqualStrings("HTTP/1.1 413 \r\n" ++ + "Connection: Close\r\n" ++ + "Content-Length: 17\r\n\r\n" ++ + "Request too large", res); } test "Client: http invalid handshake" { try assertHTTPError( - error.InvalidRequest, 400, "Invalid request", "\r\n\r\n", ); try assertHTTPError( - error.NotFound, 404, "Not found", "GET /over/9000 HTTP/1.1\r\n\r\n", ); try assertHTTPError( - error.NotFound, 404, "Not found", "POST / HTTP/1.1\r\n\r\n", ); try assertHTTPError( - error.InvalidProtocol, 400, "Invalid HTTP protocol", "GET / HTTP/1.0\r\n\r\n", ); try assertHTTPError( - error.MissingHeaders, 400, "Missing required header", "GET / HTTP/1.1\r\n\r\n", ); try assertHTTPError( - error.MissingHeaders, 400, "Missing required header", "GET / HTTP/1.1\r\nConnection: upgrade\r\n\r\n", ); try assertHTTPError( - error.MissingHeaders, 400, "Missing required header", "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\n\r\n", ); try assertHTTPError( - error.MissingHeaders, 400, "Missing required header", "GET / HTTP/1.1\r\nConnection: upgrade\r\nUpgrade: websocket\r\nsec-websocket-version:13\r\n\r\n", @@ -1225,11 +1206,8 @@ test "Client: http invalid handshake" { } test "Client: http valid handshake" { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); + var c = try createTestClient(); + defer c.deinit(); const request = "GET / HTTP/1.1\r\n" ++ @@ -1239,149 +1217,83 @@ test "Client: http valid handshake" { "sec-websocket-key: this is my key\r\n" ++ "Custom: Header-Value\r\n\r\n"; - @memcpy(client.reader.buf[0..request.len], request); - try testing.expectEqual(true, try client.processData(request.len)); - - try testing.expectEqual(.websocket, client.mode); - try testing.expectEqualStrings( - "HTTP/1.1 101 Switching Protocols\r\n" ++ - "Upgrade: websocket\r\n" ++ - "Connection: upgrade\r\n" ++ - "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", - ms.sent.items[0], - ); -} - -test "Client: http get json version" { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); - - const request = "GET /json/version HTTP/1.1\r\n\r\n"; - - @memcpy(client.reader.buf[0..request.len], request); - try testing.expectEqual(true, try client.processData(request.len)); - - try testing.expectEqual(.http, client.mode); - - // this is the hardcoded string in our MockServer - try testing.expectEqualStrings("the json version response", ms.sent.items[0]); -} - -test "Client: write websocket message" { - const cases = [_]struct { expected: []const u8, message: []const u8 }{ - .{ .expected = &.{ 129, 2, '"', '"' }, .message = "" }, - .{ .expected = [_]u8{ 129, 14 } ++ "\"hello world!\"", .message = "hello world!" }, - .{ .expected = [_]u8{ 129, 126, 0, 132 } ++ "\"" ++ ("A" ** 130) ++ "\"", .message = "A" ** 130 }, - }; - - for (cases) |c| { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); - - try client.sendJSON(c.message, .{}); - try testing.expectEqual(1, ms.sent.items.len); - try testing.expectEqualSlices(u8, c.expected, ms.sent.items[0]); - } + const res = try c.httpRequest(request); + try testing.expectEqualStrings("HTTP/1.1 101 Switching Protocols\r\n" ++ + "Upgrade: websocket\r\n" ++ + "Connection: upgrade\r\n" ++ + "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res); } test "Client: read invalid websocket message" { // 131 = 128 (fin) | 3 where 3 isn't a valid type try assertWebSocketError( - error.InvalidMessageType, 1002, - "", &.{ 131, 128, 'm', 'a', 's', 'k' }, ); for ([_]u8{ 16, 32, 64 }) |rsv| { // none of the reserve flags should be set try assertWebSocketError( - error.ReservedFlags, 1002, - "", &.{ rsv, 128, 'm', 'a', 's', 'k' }, ); // as a bitmask try assertWebSocketError( - error.ReservedFlags, 1002, - "", &.{ rsv + 4, 128, 'm', 'a', 's', 'k' }, ); } // client->server messages must be masked try assertWebSocketError( - error.NotMasked, 1002, - "", &.{ 129, 1, 'a' }, ); // control types (ping/ping/close) can't be > 125 bytes for ([_]u8{ 136, 137, 138 }) |op| { try assertWebSocketError( - error.ControlTooLarge, 1002, - "", &.{ op, 254, 1, 1 }, ); } // length of message is 0000 0401, i.e: 1024 * 256 + 1 try assertWebSocketError( - error.TooLarge, 1009, - "", &.{ 129, 255, 0, 0, 0, 0, 0, 4, 0, 1, 'm', 'a', 's', 'k' }, ); // continuation type message must come after a normal message // even when not a fin frame try assertWebSocketError( - error.InvalidContinuation, 1002, - "", &.{ 0, 129, 'm', 'a', 's', 'k', 'd' }, ); // continuation type message must come after a normal message // even as a fin frame try assertWebSocketError( - error.InvalidContinuation, 1002, - "", &.{ 128, 129, 'm', 'a', 's', 'k', 'd' }, ); // text (non-fin) - text (non-fin) try assertWebSocketError( - error.NestedFragementation, 1002, - "", &.{ 1, 129, 'm', 'a', 's', 'k', 'd', 1, 128, 'k', 's', 'a', 'm' }, ); // text (non-fin) - text (fin) should always been continuation after non-fin try assertWebSocketError( - error.NestedFragementation, 1002, - "", &.{ 1, 129, 'm', 'a', 's', 'k', 'd', 129, 128, 'k', 's', 'a', 'm' }, ); // close must be fin try assertWebSocketError( - error.InvalidContinuation, 1002, - "", &.{ 8, 129, 'm', 'a', 's', 'k', 'd', }, @@ -1389,9 +1301,7 @@ test "Client: read invalid websocket message" { // ping must be fin try assertWebSocketError( - error.InvalidContinuation, 1002, - "", &.{ 9, 129, 'm', 'a', 's', 'k', 'd', }, @@ -1399,9 +1309,7 @@ test "Client: read invalid websocket message" { // pong must be fin try assertWebSocketError( - error.InvalidContinuation, 1002, - "", &.{ 10, 129, 'm', 'a', 's', 'k', 'd', }, @@ -1436,159 +1344,6 @@ test "Client: close message" { ); } -// Testing both HTTP and websocket messages broken up across multiple reads. -// We need to fuzz HTTP messages differently than websocket. HTTP are strictly -// req -> res with no pipelining. So there should only be 1 message at a time. -// So we can only "fuzz" on a per-message basis. -// But for websocket, we can fuzz _all_ the messages together. -test "Client: fuzz" { - var prng = std.rand.DefaultPrng.init(blk: { - var seed: u64 = undefined; - try std.posix.getrandom(std.mem.asBytes(&seed)); - break :blk seed; - }); - const random = prng.random(); - - const allocator = testing.allocator; - var websocket_messages: std.ArrayListUnmanaged(u8) = .{}; - defer websocket_messages.deinit(allocator); - - // ping with no payload - try websocket_messages.appendSlice( - allocator, - &.{ 137, 128, 0, 0, 0, 0 }, - ); - - // // 10 byte text message with a 0,0,0,0 mask - try websocket_messages.appendSlice( - allocator, - &.{ 129, 138, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, - ); - - // ping with a payload - try websocket_messages.appendSlice( - allocator, - &.{ 137, 133, 0, 5, 7, 10, 100, 101, 102, 103, 104 }, - ); - - // pong with no payload (noop in the server) - try websocket_messages.appendSlice( - allocator, - &.{ 138, 128, 10, 10, 10, 10 }, - ); - - // 687 long message, with a mask - try websocket_messages.appendSlice( - allocator, - [_]u8{ 129, 254, 2, 175, 1, 2, 3, 4 } ++ "A" ** 687, - ); - - // non-fin text message - try websocket_messages.appendSlice(allocator, &.{ 1, 130, 0, 0, 0, 0, 1, 2 }); - - // continuation - try websocket_messages.appendSlice(allocator, &.{ 0, 131, 0, 0, 0, 0, 3, 4, 5 }); - - // pong happening in fragement - try websocket_messages.appendSlice(allocator, &.{ 138, 128, 0, 0, 0, 0 }); - - // more continuation - try websocket_messages.appendSlice(allocator, &.{ 0, 130, 0, 0, 0, 0, 6, 7 }); - - // fin - try websocket_messages.appendSlice(allocator, &.{ 128, 133, 0, 0, 0, 0, 8, 9, 10, 11, 12 }); - - // close - try websocket_messages.appendSlice( - allocator, - &.{ 136, 130, 200, 103, 34, 22, 0, 1 }, - ); - - const SendRandom = struct { - fn send(c: anytype, r: std.Random, data: []const u8) !void { - var buf = data; - while (buf.len > 0) { - const to_send = r.intRangeAtMost(usize, 1, buf.len); - @memcpy(c.readBuf()[0..to_send], buf[0..to_send]); - if (try c.processData(to_send) == false) { - return; - } - buf = buf[to_send..]; - } - } - }; - - for (0..100) |_| { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); - - try SendRandom.send(&client, random, "GET /json/version HTTP/1.1\r\nContent-Length: 0\r\n\r\n"); - try SendRandom.send(&client, random, "GET / HTTP/1.1\r\n" ++ - "Connection: upgrade\r\n" ++ - "Upgrade: websocket\r\n" ++ - "sec-websocket-version:13\r\n" ++ - "sec-websocket-key: 1234aa93\r\n" ++ - "Custom: Header-Value\r\n\r\n"); - - // fuzz over all websocket messages - try SendRandom.send(&client, random, websocket_messages.items); - - try testing.expectEqual(5, ms.sent.items.len); - - try testing.expectEqualStrings( - "the json version response", - ms.sent.items[0], - ); - - try testing.expectEqualStrings( - "HTTP/1.1 101 Switching Protocols\r\n" ++ - "Upgrade: websocket\r\n" ++ - "Connection: upgrade\r\n" ++ - "Sec-Websocket-Accept: KnOKWrrjHS0nGFmtfmYFQoPIGKQ=\r\n\r\n", - ms.sent.items[1], - ); - - try testing.expectEqualSlices(u8, &.{ 138, 0 }, ms.sent.items[2]); - - try testing.expectEqualSlices( - u8, - &.{ 138, 5, 100, 96, 97, 109, 104 }, - ms.sent.items[3], - ); - - try testing.expectEqualSlices( - u8, - &.{ 136, 2, 3, 232 }, - ms.sent.items[4], - ); - - const received = client.cdp.?.messages.items; - try testing.expectEqual(3, received.len); - try testing.expectEqualSlices( - u8, - &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, - received[0], - ); - - try testing.expectEqualSlices( - u8, - &([_]u8{ 64, 67, 66, 69 } ** 171 ++ [_]u8{ 64, 67, 66 }), - received[1], - ); - - try testing.expectEqualSlices( - u8, - &.{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, - received[2], - ); - - try testing.expectEqual(true, ms.closed); - } -} - test "server: mask" { var buf: [4000]u8 = undefined; const messages = [_][]const u8{ "1234", "1234" ** 99, "1234" ** 999 }; @@ -1649,129 +1404,54 @@ test "server: get /json/version" { } fn assertHTTPError( - expected_error: anyerror, comptime expected_status: u16, comptime expected_body: []const u8, input: []const u8, ) !void { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); - - @memcpy(client.reader.buf[0..input.len], input); - try testing.expectError(expected_error, client.processData(input.len)); + var c = try createTestClient(); + defer c.deinit(); + const res = try c.httpRequest(input); const expected_response = std.fmt.comptimePrint( "HTTP/1.1 {d} \r\nConnection: Close\r\nContent-Length: {d}\r\n\r\n{s}", .{ expected_status, expected_body.len, expected_body }, ); - try testing.expectEqual(1, ms.sent.items.len); - try testing.expectEqualStrings(expected_response, ms.sent.items[0]); + try testing.expectEqualStrings(expected_response, res); } -fn assertWebSocketError( - expected_error: anyerror, - close_code: u16, - close_payload: []const u8, - input: []const u8, -) !void { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); - - client.mode = .websocket; // force websocket message processing - - @memcpy(client.reader.buf[0..input.len], input); - try testing.expectError(expected_error, client.processData(input.len)); - - try testing.expectEqual(1, ms.sent.items.len); - - const actual = ms.sent.items[0]; - - // fin | close opcode - try testing.expectEqual(136, actual[0]); - - // message length (code + payload) - try testing.expectEqual(2 + close_payload.len, actual[1]); +fn assertWebSocketError(close_code: u16, input: []const u8) !void { + var c = try createTestClient(); + defer c.deinit(); - // close code - try testing.expectEqual(close_code, std.mem.readInt(u16, actual[2..4], .big)); + try c.handshake(); + try c.stream.writeAll(input); - // close payload (if any) - try testing.expectEqualStrings(close_payload, actual[4..]); -} + const msg = try c.readWebsocketMessage() orelse return error.NoMessage; + defer if (msg.cleanup_fragment) { + c.reader.cleanup(); + }; -fn assertWebSocketMessage( - expected: []const u8, - input: []const u8, -) !void { - var ms = MockServer{}; - defer ms.deinit(); - - var client = ClientT(*MockServer, MockCDP).init(0, &ms); - defer client.deinit(); - client.mode = .websocket; // force websocket message processing - - @memcpy(client.reader.buf[0..input.len], input); - const more = try client.processData(input.len); - - try testing.expectEqual(1, ms.sent.items.len); - try testing.expectEqualSlices(u8, expected, ms.sent.items[0]); - - // if we sent a close message, then the serve should have been told - // to close the connection - if (expected[0] == 136) { - try testing.expectEqual(true, ms.closed); - try testing.expectEqual(false, more); - } else { - try testing.expectEqual(false, ms.closed); - try testing.expectEqual(true, more); - } + try testing.expectEqual(.close, msg.type); + try testing.expectEqual(2, msg.data.len); + try testing.expectEqual(close_code, std.mem.readInt(u16, msg.data[0..2], .big)); } -const MockServer = struct { - loop: *jsruntime.Loop = undefined, - closed: bool = false, - - // record the messages we sent to the client - sent: std.ArrayListUnmanaged([]const u8) = .{}, - - allocator: Allocator = testing.allocator, - - json_version_response: []const u8 = "the json version response", - - fn deinit(self: *MockServer) void { - const allocator = self.allocator; +fn assertWebSocketMessage(expected: []const u8, input: []const u8) !void { + var c = try createTestClient(); + defer c.deinit(); - for (self.sent.items) |msg| { - allocator.free(msg); - } - self.sent.deinit(allocator); - } + try c.handshake(); + try c.stream.writeAll(input); - fn queueClose(self: *MockServer, _: anytype) void { - self.closed = true; - } + const msg = try c.readWebsocketMessage() orelse return error.NoMessage; + defer if (msg.cleanup_fragment) { + c.reader.cleanup(); + }; - fn queueSend( - self: *MockServer, - socket: posix.socket_t, - arena: ?ArenaAllocator, - data: []const u8, - ) !void { - _ = socket; - const owned = try self.allocator.dupe(u8, data); - try self.sent.append(self.allocator, owned); - if (arena) |a| { - a.deinit(); - } - } -}; + const actual = c.reader.buf[0 .. msg.data.len + 2]; + try testing.expectEqualSlices(u8, expected, actual); +} const MockCDP = struct { messages: std.ArrayListUnmanaged([]const u8) = .{}, @@ -1809,15 +1489,20 @@ fn createTestClient() !TestClient { }); try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.RCVTIMEO, &timeout); try posix.setsockopt(stream.handle, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); - return .{ .stream = stream }; + return .{ + .stream = stream, + .reader = .{ .allocator = testing.allocator }, + }; } const TestClient = struct { stream: std.net.Stream, buf: [1024]u8 = undefined, + reader: Reader(false), fn deinit(self: *TestClient) void { self.stream.close(); + self.reader.deinit(); } fn httpRequest(self: *TestClient, req: []const u8) ![]const u8 { @@ -1827,21 +1512,27 @@ const TestClient = struct { var total_length: ?usize = null; while (true) { pos += try self.stream.read(self.buf[pos..]); + if (pos == 0) { + return error.NoMoreData; + } const response = self.buf[0..pos]; if (total_length == null) { const header_end = std.mem.indexOf(u8, response, "\r\n\r\n") orelse continue; const header = response[0 .. header_end + 4]; - const cl_header = "Content-Length: "; - const start = (std.mem.indexOf(u8, header, cl_header) orelse { - return error.MissingContentLength; - }) + cl_header.len; + const cl = blk: { + const cl_header = "Content-Length: "; + const start = (std.mem.indexOf(u8, header, cl_header) orelse { + break :blk 0; + }) + cl_header.len; - const end = std.mem.indexOfScalarPos(u8, header, start, '\r') orelse { - return error.InvalidContentLength; - }; - const cl = std.fmt.parseInt(usize, header[start..end], 10) catch { - return error.InvalidContentLength; + const end = std.mem.indexOfScalarPos(u8, header, start, '\r') orelse { + return error.InvalidContentLength; + }; + + break :blk std.fmt.parseInt(usize, header[start..end], 10) catch { + return error.InvalidContentLength; + }; }; total_length = cl + header.len; @@ -1857,4 +1548,33 @@ const TestClient = struct { } } } + + fn handshake(self: *TestClient) !void { + const request = + "GET / HTTP/1.1\r\n" ++ + "Connection: upgrade\r\n" ++ + "Upgrade: websocket\r\n" ++ + "sec-websocket-version:13\r\n" ++ + "sec-websocket-key: this is my key\r\n" ++ + "Custom: Header-Value\r\n\r\n"; + + const res = try self.httpRequest(request); + try testing.expectEqualStrings("HTTP/1.1 101 Switching Protocols\r\n" ++ + "Upgrade: websocket\r\n" ++ + "Connection: upgrade\r\n" ++ + "Sec-Websocket-Accept: flzHu2DevQ2dSCSVqKSii5e9C2o=\r\n\r\n", res); + } + + fn readWebsocketMessage(self: *TestClient) !?Message { + while (true) { + const n = try self.stream.read(self.reader.readBuf()); + if (n == 0) { + return error.Closed; + } + self.reader.len += n; + if (try self.reader.next()) |msg| { + return msg; + } + } + } };