Como Parsear URLs em Zig

Introdução

Parsear URLs (Uniform Resource Locators) é uma tarefa fundamental em programação web e de rede. Em Zig, a biblioteca padrão oferece std.Uri para decompor URLs em seus componentes: esquema, host, porta, caminho, query string e fragmento.

Nesta receita, você aprenderá a parsear, construir e manipular URLs em Zig.

Pré-requisitos

Parsear uma URL Completa

Use std.Uri.parse para decompor uma URL em seus componentes:

const std = @import("std");

pub fn main() !void {
    const url = "https://api.exemplo.com:8443/v2/users?page=1&limit=20#resultados";

    const uri = try std.Uri.parse(url);

    std.debug.print("URL completa: {s}\n\n", .{url});
    std.debug.print("Esquema:    {s}\n", .{uri.scheme});
    std.debug.print("Host:       {s}\n", .{uri.host.?.percent_encoded});
    std.debug.print("Porta:      {?d}\n", .{uri.port});
    std.debug.print("Caminho:    {s}\n", .{uri.path.percent_encoded});
    std.debug.print("Query:      {s}\n", .{uri.query.?.percent_encoded});
    std.debug.print("Fragmento:  {s}\n", .{uri.fragment.?.percent_encoded});
}

Saída esperada

URL completa: https://api.exemplo.com:8443/v2/users?page=1&limit=20#resultados

Esquema:    https
Host:       api.exemplo.com
Porta:      8443
Caminho:    /v2/users
Query:      page=1&limit=20
Fragmento:  resultados

Parsear Query String

Extraia parâmetros individuais da query string:

const std = @import("std");

const QueryParam = struct {
    key: []const u8,
    value: []const u8,
};

fn parseQueryString(
    allocator: std.mem.Allocator,
    query: []const u8,
) ![]QueryParam {
    var params = std.ArrayList(QueryParam).init(allocator);
    errdefer params.deinit();

    var iter = std.mem.splitScalar(u8, query, '&');
    while (iter.next()) |pair| {
        if (pair.len == 0) continue;

        if (std.mem.indexOfScalar(u8, pair, '=')) |eq_pos| {
            try params.append(.{
                .key = pair[0..eq_pos],
                .value = pair[eq_pos + 1 ..],
            });
        } else {
            try params.append(.{
                .key = pair,
                .value = "",
            });
        }
    }

    return params.toOwnedSlice();
}

fn getParam(params: []const QueryParam, key: []const u8) ?[]const u8 {
    for (params) |p| {
        if (std.mem.eql(u8, p.key, key)) {
            return p.value;
        }
    }
    return null;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const url = "https://busca.exemplo.com/search?q=zig+lang&page=2&lang=pt-BR&safe=on";
    const uri = try std.Uri.parse(url);

    const query = uri.query.?.percent_encoded;
    const params = try parseQueryString(allocator, query);
    defer allocator.free(params);

    std.debug.print("Parâmetros da URL:\n", .{});
    for (params) |p| {
        std.debug.print("  {s} = {s}\n", .{ p.key, p.value });
    }

    // Acessar parâmetro específico
    if (getParam(params, "q")) |valor| {
        std.debug.print("\nBusca por: {s}\n", .{valor});
    }

    if (getParam(params, "page")) |valor| {
        std.debug.print("Página: {s}\n", .{valor});
    }
}

Saída esperada

Parâmetros da URL:
  q = zig+lang
  page = 2
  lang = pt-BR
  safe = on

Busca por: zig+lang
Página: 2

Construir URLs

Monte URLs a partir de componentes:

const std = @import("std");

fn buildUrl(
    allocator: std.mem.Allocator,
    scheme: []const u8,
    host: []const u8,
    port: ?u16,
    path: []const u8,
    query_params: ?[]const [2][]const u8,
) ![]u8 {
    var url = std.ArrayList(u8).init(allocator);
    errdefer url.deinit();

    const writer = url.writer();

    // Esquema
    try writer.print("{s}://", .{scheme});

    // Host
    try writer.writeAll(host);

    // Porta (se especificada)
    if (port) |p| {
        try writer.print(":{d}", .{p});
    }

    // Caminho
    if (path.len > 0 and path[0] != '/') {
        try writer.writeByte('/');
    }
    try writer.writeAll(path);

    // Query string
    if (query_params) |params| {
        try writer.writeByte('?');
        for (params, 0..) |param, i| {
            if (i > 0) try writer.writeByte('&');
            try writer.print("{s}={s}", .{ param[0], param[1] });
        }
    }

    return url.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const params = [_][2][]const u8{
        .{ "formato", "json" },
        .{ "idioma", "pt-BR" },
        .{ "limite", "50" },
    };

    const url = try buildUrl(
        allocator,
        "https",
        "api.exemplo.com",
        8443,
        "/v2/dados",
        &params,
    );
    defer allocator.free(url);

    std.debug.print("URL construída: {s}\n", .{url});
}

Saída esperada

URL construída: https://api.exemplo.com:8443/v2/dados?formato=json&idioma=pt-BR&limite=50

Decodificar URL Percent-Encoding

Decodifique caracteres especiais em URLs:

const std = @import("std");

fn urlDecode(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
    var result = std.ArrayList(u8).init(allocator);
    errdefer result.deinit();

    var i: usize = 0;
    while (i < input.len) {
        if (input[i] == '%' and i + 2 < input.len) {
            const high = std.fmt.charToDigit(input[i + 1], 16) catch {
                try result.append(input[i]);
                i += 1;
                continue;
            };
            const low = std.fmt.charToDigit(input[i + 2], 16) catch {
                try result.append(input[i]);
                i += 1;
                continue;
            };
            try result.append(@as(u8, high) << 4 | low);
            i += 3;
        } else if (input[i] == '+') {
            try result.append(' ');
            i += 1;
        } else {
            try result.append(input[i]);
            i += 1;
        }
    }

    return result.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const encoded = "Ol%C3%A1+mundo%21+Programa%C3%A7%C3%A3o+em+Zig";
    const decoded = try urlDecode(allocator, encoded);
    defer allocator.free(decoded);

    std.debug.print("Codificado: {s}\n", .{encoded});
    std.debug.print("Decodificado: {s}\n", .{decoded});
}

Saída esperada

Codificado: Ol%C3%A1+mundo%21+Programa%C3%A7%C3%A3o+em+Zig
Decodificado: Olá mundo! Programação em Zig

Validar URLs

Verifique se uma string é uma URL válida:

const std = @import("std");

fn isValidUrl(url: []const u8) bool {
    const uri = std.Uri.parse(url) catch return false;

    // Verificar se tem esquema
    if (uri.scheme.len == 0) return false;

    // Verificar se tem host
    if (uri.host == null) return false;

    // Verificar esquemas permitidos
    const valid_schemes = [_][]const u8{ "http", "https", "ftp", "ws", "wss" };
    for (&valid_schemes) |scheme| {
        if (std.mem.eql(u8, uri.scheme, scheme)) return true;
    }

    return false;
}

pub fn main() void {
    const urls = [_][]const u8{
        "https://www.exemplo.com/pagina",
        "http://localhost:8080",
        "ftp://arquivos.exemplo.com/doc.pdf",
        "não é uma url",
        "mailto:email@exemplo.com",
        "ws://chat.exemplo.com/ws",
    };

    for (&urls) |url| {
        const valid = isValidUrl(url);
        std.debug.print("{s}: {s}\n", .{
            url,
            if (valid) "VÁLIDA" else "INVÁLIDA",
        });
    }
}

Dicas e Boas Práticas

  1. Use std.Uri.parse: Não tente parsear URLs manualmente; a biblioteca padrão trata casos complexos.

  2. Cuidado com encoding: URLs podem conter caracteres percent-encoded. Sempre decodifique antes de usar.

  3. Valide antes de usar: Sempre verifique se a URL é válida antes de tentar conectar.

  4. Porta padrão: Se a porta não estiver na URL, use a porta padrão do esquema (80 para HTTP, 443 para HTTPS).

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.