//! The `hosts(5)` blocklist format: an optional sink address followed by one or
//! more names. Pure, `std` only.
//!
//! The sink address is not checked against a list of "blocking" addresses: a
//! list that maps to `127.0.0.1`, `0.0.0.0` or `::` carries the same
//! instruction, and a list that maps to a real address is still a set of names
//! the operator asked to block.

const std = @import("std");
const parsers = @import("parsers.zig");

pub fn parseLine(line: []const u8) parsers.Line {
    const uncommented = if (std.mem.findScalar(u8, line, '#')) |at| line[0..at] else line;
    const text = std.mem.trim(u8, uncommented, &std.ascii.whitespace);
    if (text.len == 0) return .{ .kind = .ignore };
    if (text[0] == '/') return .{ .kind = .regex };

    const split = std.mem.findAny(u8, text, &std.ascii.whitespace) orelse {
        // A bare address with no name is a hosts line that names nothing to
        // block; it is counted rather than compiled into an entry.
        if (parsers.looksLikeIpLiteral(text)) return .{ .kind = .unsupported };
        return .{ .kind = .domain, .text = text };
    };

    // Lists that are bare name lists with a hosts extension are common, so a
    // first field that is not an address is a name like any other.
    if (!parsers.looksLikeIpLiteral(text[0..split])) return .{ .kind = .domain, .text = text };

    const names = std.mem.trimStart(u8, text[split..], &std.ascii.whitespace);
    return .{ .kind = .domain, .text = names };
}

const testing = std.testing;

test "a sink address and one name" {
    const line = parseLine("0.0.0.0 ads.example.com");
    try testing.expectEqual(parsers.Kind.domain, line.kind);
    try testing.expectEqualStrings("ads.example.com", line.text);
}

test "a sink address and several names" {
    const line = parseLine("127.0.0.1 a.example.com b.example.com");
    try testing.expectEqual(parsers.Kind.domain, line.kind);
    try testing.expectEqualStrings("a.example.com b.example.com", line.text);
}

test "single-label names survive the parser" {
    const line = parseLine("::1 ip6-localhost ip6-loopback");
    try testing.expectEqual(parsers.Kind.domain, line.kind);
    try testing.expectEqualStrings("ip6-localhost ip6-loopback", line.text);
}

test "a trailing comment is removed" {
    const line = parseLine("0.0.0.0 ads.example.com # tracker");
    try testing.expectEqual(parsers.Kind.domain, line.kind);
    try testing.expectEqualStrings("ads.example.com", line.text);
}

test "a comment line is ignored" {
    try testing.expectEqual(parsers.Kind.ignore, parseLine("# whole line").kind);
}

test "an empty line is ignored" {
    try testing.expectEqual(parsers.Kind.ignore, parseLine("").kind);
}

test "a blank line is ignored" {
    try testing.expectEqual(parsers.Kind.ignore, parseLine("   ").kind);
}

test "a regex line is recognized" {
    try testing.expectEqual(parsers.Kind.regex, parseLine("/ads\\d+/").kind);
}

test "a bare name without a sink address" {
    const line = parseLine("example.com");
    try testing.expectEqual(parsers.Kind.domain, line.kind);
    try testing.expectEqualStrings("example.com", line.text);
}

test "a bare sink address names nothing" {
    try testing.expectEqual(parsers.Kind.unsupported, parseLine("0.0.0.0").kind);
}
