From 57982c29668ffb45fb0b7a574fee34261978453a Mon Sep 17 00:00:00 2001 From: Ekaitz Zarraga Date: Sun, 9 Apr 2023 22:28:36 +0200 Subject: Some http random parser and stuff --- build.zig | 34 +++++ src/http.zig | 392 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 39 +++++ src/weatherFetcher.zig | 101 +++++++++++++ 4 files changed, 566 insertions(+) create mode 100644 build.zig create mode 100644 src/http.zig create mode 100644 src/main.zig create mode 100644 src/weatherFetcher.zig diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..834c43a --- /dev/null +++ b/build.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +pub fn build(b: *std.build.Builder) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard release options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("elTiempo", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_tests = b.addTest("src/main.zig"); + exe_tests.setTarget(target); + exe_tests.setBuildMode(mode); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&exe_tests.step); +} diff --git a/src/http.zig b/src/http.zig new file mode 100644 index 0000000..49b4e71 --- /dev/null +++ b/src/http.zig @@ -0,0 +1,392 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const split = std.mem.split; +const net = std.net; +const expect = std.testing.expect; +const expectError = std.testing.expectError; +const StringHashMap = std.StringHashMap; + +pub const HttpError = error { + ParsingError +}; + +/// HttpStatus +pub const HttpStatus = struct{ + protocol: []const u8, + status: usize, + status_msg: []const u8, +}; + +pub const HttpStatusParser = struct{ + status: HttpStatus, + rawline: []u8, + allocator: Allocator, + + const Self = @This(); + pub fn init(allocator: Allocator) Self{ + return Self{ + .status = undefined, // Can I do this? + .rawline = "", + .allocator = allocator, + }; + } + /// Takes a reader and consumes one status line of HTTP response allocating + /// it's own copy of the line. Must call `deinit()` to clean the + /// allocations. + /// Fills it's `status` field with the results of that. + pub fn parseReader(self: *Self, reader: anytype) !void{ + self.rawline = try reader.readUntilDelimiterAlloc(self.allocator, '\r', 1000); + // Drop \n + _ = try reader.readByte(); + + var components = split(u8, self.rawline, " "); + + const p = if(components.next()) |p| p else return error.ParsingError; + const s = if(components.next()) |s| s else return error.ParsingError; + const sm = components.rest(); + if (sm.len == 0) return error.ParsingError; + self.status.protocol = p; + self.status.status = try std.fmt.parseUnsigned(usize, s, 10); + self.status.status_msg = sm; + } + pub fn deinit(self: *Self) void{ + self.allocator.free(self.rawline); + } +}; + + +test "200 OK is parsed correctly in an HttpStatus" { + const status_line = "HTTP/1.1 200 OK\r\n"; + const allocator = std.testing.allocator; + + var fis = std.io.fixedBufferStream(status_line); + const reader = fis.reader(); + + var status = HttpStatusParser.init(allocator); + defer status.deinit(); + try status.parseReader(reader); + + try expect( std.mem.eql(u8, status.status.protocol, "HTTP/1.1") ); + try expect( status.status.status == 200 ); + try expect( std.mem.eql(u8, status.status.status_msg, "OK") ); + +} + +test "306 Switch Proxy is parsed correctly in an HttpStatus" { + const status_line = "HTTP/1.1 306 Switch Proxy\r\n"; + const allocator = std.testing.allocator; + + var fis = std.io.fixedBufferStream(status_line); + const reader = fis.reader(); + + var status = HttpStatusParser.init(allocator); + defer status.deinit(); + try status.parseReader(reader); + + try expect( std.mem.eql(u8, status.status.protocol, "HTTP/1.1") ); + try expect( status.status.status == 306 ); + try expect( std.mem.eql(u8, status.status.status_msg, "Switch Proxy") ); +} + +test "Broken statusline is detected correctly in an HttpStatus" { + const status_line = "HTTP/1.1 306 \r\n"; + const allocator = std.testing.allocator; + var fis = std.io.fixedBufferStream(status_line); + const reader = fis.reader(); + + var status = HttpStatusParser.init(allocator); + defer status.deinit(); + try expectError( error.ParsingError, status.parseReader(reader) ); +} + + +/// HttpHeaders + +pub const HttpHeaders = struct{ + values: StringHashMap([]const u8), + owned_data: std.ArrayList([]u8), + allocator: Allocator, + + const Self = @This(); + pub fn init(allocator: Allocator) Self{ + return Self{ + .values = StringHashMap([]const u8).init(allocator), + .allocator = allocator, + .owned_data = std.ArrayList([]u8).init(allocator), + }; + } + pub fn get(self: *Self, k: []const u8) !?[]const u8{ + var key:[]u8 = try self.allocator.alloc(u8, k.len); + defer self.allocator.free(key); + for (key) |*char, index|{ + char.* = std.ascii.toLower(k[index]); + } + return self.values.get(key); + } + pub fn put(self: *Self, k: []const u8, v: []const u8) !void{ + // Own key + var key:[]u8 = try self.allocator.alloc(u8, k.len); + for (key) |*char,index|{ + char.* = std.ascii.toLower(char.*); + char.* = std.ascii.toLower(k[index]); + } + try self.owned_data.append(key); + + // Own value + var value:[]u8 = try self.allocator.alloc(u8, v.len); + std.mem.copy(u8, value, v); + try self.owned_data.append(value); + + // Store header + try self.values.put(key, value); + } + pub fn deinit(self: *Self) void{ + self.values.deinit(); + for (self.owned_data.items) |item|{ + self.allocator.free(item); + } + self.owned_data.deinit(); + } +}; + +pub const HttpHeaderParser = struct{ + headers: HttpHeaders, + allocator: Allocator, + + const Self = @This(); + pub fn init(allocator: Allocator) Self{ + return Self{ + .headers = HttpHeaders.init(allocator), + .allocator = allocator, + }; + } + pub fn parseReader(self: *Self, reader: anytype) !void{ + while(true) { + var line:[100000]u8 = undefined; + var slice = try reader.readUntilDelimiter(&line, '\r'); + // Drop \n + _ = try reader.readByte(); + + if(slice.len == 0){ + return; // Finished header block + } + var hit = std.mem.split(u8, slice, ":"); + const key = if (hit.next()) |k| k else return error.ParsingError; + const val = hit.rest(); + if (val.len == 0){ + return error.ParsingError; + } + var spaces:usize = 0; + for(val) |ch|{ + if(ch == ' '){ + spaces = spaces + 1; + } else { + break; + } + } else { + return error.ParsingError; + } + + try self.headers.put(key, val[spaces..]); + } + } + pub fn deinit(self: *Self) void{ + self.headers.deinit(); + } +}; + + + +test "Header block is parsed correctly, is case-insensitive and ignores spaces after :" { + const allocator = std.testing.allocator; + + const block = + "Host: api.open-meteo.com\r\n" ++ + "Transfer-Encoding: chunked\r\n" ++ + "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0\r\n" ++ + "\r\n"; + var fis = std.io.fixedBufferStream(block); + const reader = fis.reader(); + + var headers = HttpHeaderParser.init(allocator); + defer headers.deinit(); + + try headers.parseReader(reader); + if (try headers.headers.get("HOST")) |host| { + try expect( std.mem.eql(u8, host, "api.open-meteo.com") ); + } + if (try headers.headers.get("host")) |host| { + try expect( std.mem.eql(u8, host, "api.open-meteo.com") ); + } + if (try headers.headers.get("Transfer-Encoding")) |tencoding| { + try expect( std.mem.eql(u8, tencoding, "chunked") ); + } + if (try headers.headers.get("User-Agent")) |useragent| { + try expect( std.mem.eql(u8, useragent, + "Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0") ); + } +} + +test "Header line with no semicolon is a parsing error" { + const allocator = std.testing.allocator; + + const block = + "Host api.open-meteo.com\r\n" ++ + "Transfer-Encoding: chunked\r\n" ++ + "\r\n"; + var fis = std.io.fixedBufferStream(block); + const reader = fis.reader(); + + var headers = HttpHeaderParser.init(allocator); + defer headers.deinit(); + + try expectError(error.ParsingError, headers.parseReader(reader)); +} + + +/// HttpBody + +pub const HttpBodyParser = struct { + data: []u8, + allocator: Allocator, + + const Self = @This(); + pub fn init(allocator: Allocator) !Self{ + return Self{ + .data = try allocator.create([0]u8), + .allocator = allocator, + }; + } + pub fn deinit(self: *Self) void{ + self.allocator.free(self.data); + } + pub fn parseChunkedReader(self: *Self, body_reader: anytype) !void{ + var cursor:usize = 0; + while (true){ + var buffer:[100]u8 = undefined; + // Read chunk size line + var size = try body_reader.readUntilDelimiter(&buffer, '\r'); + _ = try body_reader.readByte(); // Drop \n + // Parse the number + const chunk_size = try std.fmt.parseUnsigned(usize, size, 16); + // If chunk is empty finish + if( chunk_size == 0 ){ return; } + + // Reallocate the data to fit the incoming chunk + self.data = try self.allocator.realloc(self.data, cursor + chunk_size); + // Read all the bytes of the chunk + for( self.data[cursor..cursor+chunk_size] ) |*byte|{ + byte.* = try body_reader.readByte(); + } + // Drop \r\n + _ = try body_reader.readByte(); + _ = try body_reader.readByte(); + cursor = cursor + chunk_size; + } + } + pub fn parseFixedSizeReader(self: *Self, body_reader: anytype, size: usize) !void{ + self.allocator.free(self.data); + self.data = try body_reader.readAllAlloc(self.allocator, size); + } +}; + +test "Parse chunked body from Reader"{ + const chunked_body = + "7\r\n" ++ + "Mozilla\r\n" ++ + "12\r\n" ++ + " Developer Network\r\n" ++ + "0\r\n" ++ + "\r\n".*; + var fis = std.io.fixedBufferStream(chunked_body); + const reader = fis.reader(); + + const allocator = std.testing.allocator; + var body = try HttpBodyParser.init(allocator); + defer body.deinit(); + + try body.parseChunkedReader(reader); + + try expect( std.mem.eql(u8, "Mozilla Developer Network", body.data) ); +} + +/// HttpResponse + +pub const HttpResponseParser = struct{ + statusparser: HttpStatusParser, + headerparser: HttpHeaderParser, + bodyparser: HttpBodyParser, + + const Self = @This(); + pub fn init(allocator: Allocator) !Self{ + return Self{ + .statusparser = HttpStatusParser.init(allocator), + .headerparser = HttpHeaderParser.init(allocator), + .bodyparser = try HttpBodyParser.init(allocator), + }; + } + pub fn parseReader(self: *Self, reader: anytype) !void{ + try self.statusparser.parseReader(reader); + try self.headerparser.parseReader(reader); + if(try self.headerparser.headers.get("Transfer-Encoding")) |te|{ + if( std.mem.containsAtLeast(u8, te, 1, "chunked") ) { + try self.bodyparser.parseChunkedReader(reader); + } + } else { + if(try self.headerparser.headers.get("Content-Length")) |cl|{ + const content_length = try std.fmt.parseUnsigned(usize, cl, 10); + try self.bodyparser.parseFixedSizeReader(reader, content_length); + } + } + } + pub fn deinit(self: *Self) void{ + self.statusparser.deinit(); + self.headerparser.deinit(); + self.bodyparser.deinit(); + } + + pub fn body(self: Self) []u8{ + return self.bodyparser.data; + } + pub fn headers(self: Self) HttpHeaders{ + return self.headerparser.headers; + } + pub fn status(self: Self) usize{ + return self.statusparser.status.status; + } + pub fn statusMsg(self: Self) []const u8{ + return self.statusparser.status.status_msg; + } + pub fn protocol(self: Self) []const u8{ + return self.statusparser.status.protocol; + } +}; + +// Full answer parser + +test "Parse full response correctly" { + const responsedata = + "HTTP/1.1 200 OK\r\n" ++ + "Content-Type: text/plain\r\n" ++ + "Transfer-Encoding: chunked\r\n" ++ + "\r\n" ++ + "7\r\n" ++ + "Mozilla\r\n" ++ + "12\r\n" ++ + " Developer Network\r\n" ++ + "0\r\n" ++ + "\r\n"; + + var fis = std.io.fixedBufferStream(responsedata); + const reader = fis.reader(); + + const allocator = std.testing.allocator; + var response = try HttpResponseParser.init(allocator); + defer response.deinit(); + + try response.parseReader(reader); + + try expect( std.mem.eql(u8, response.body(), "Mozilla Developer Network") ); + try expect( response.status() == 200 ); + try expect( std.mem.eql(u8, response.statusMsg(), "OK") ); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..7cf9729 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,39 @@ +const std = @import("std"); +const weatherFetcher = @import("weatherFetcher.zig"); + +pub fn main() !void{ + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + _ = try weatherFetcher.getData(allocator); + + const stdout = std.io.getStdOut().writer(); + try stdout.print("Weather app {s}\n", .{args}); +} + +//pub fn main() !void { +// // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) +// std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); +// +// // stdout is for the actual output of your application, for example if you +// // are implementing gzip, then only the compressed bytes should be sent to +// // stdout, not any debugging messages. +// const stdout_file = std.io.getStdOut().writer(); +// var bw = std.io.bufferedWriter(stdout_file); +// const stdout = bw.writer(); +// +// try stdout.print("Run `zig build test` to run the tests.\n", .{}); +// +// try bw.flush(); // don't forget to flush! +//} +// +//test "simple test" { +// var list = std.ArrayList(i32).init(std.testing.allocator); +// defer list.deinit(); // try commenting this out and see if zig detects the memory leak! +// try list.append(42); +// try std.testing.expectEqual(@as(i32, 42), list.pop()); +//} diff --git a/src/weatherFetcher.zig b/src/weatherFetcher.zig new file mode 100644 index 0000000..be99d5c --- /dev/null +++ b/src/weatherFetcher.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const net = std.net; + +const expect = std.testing.expect; + +const http = @import("./http.zig"); + + +pub const DailyDataSchema = struct { + latitude: f64, + longitude: f64, + generationtime_ms: f64, + utc_offset_seconds: i64, + timezone: []u8, + timezone_abbreviation: []u8, + elevation: f64, + hourly_units: struct{ + time: []u8, + temperature_2m: []u8, + }, + hourly: struct { + time: [][]u8, + temperature_2m: []f64, + }, +}; + +pub const DailyData = struct{ + data: DailyDataSchema, + allocator: std.mem.Allocator, + + const Self = @This(); + pub fn parse(allocator: Allocator, body: []u8) !Self{ + var stream = std.json.TokenStream.init(body); + return Self{ + .data = try std.json.parse(DailyDataSchema, &stream, .{ + .allocator = allocator + }), + .allocator = allocator, + }; + } + pub fn deinit(self: *Self) void{ + std.json.parseFree(DailyDataSchema, self.data, .{ + .allocator = self.allocator, + }); + } +}; + +/// Gets Daily Data. Allocates and returns an DailyData struct that must be +/// .deinit() later +pub fn getDailyData(allocator: std.mem.Allocator) !DailyData { + const stream = try net.tcpConnectToHost(allocator, "api.open-meteo.com", 80); + defer stream.close(); + + const writer = stream.writer(); + try writer.writeAll( "GET /v1/forecast" ++ + "?latitude=52.52" ++ + "&longitude=13.41" ++ + "&hourly=temperature_2m" ++ " HTTP/1.1\r\n" ++ + "Host: api.open-meteo.com\r\n" ++ + "Connection: keep-alive\r\n" ++ + "Accept-Encoding: identity\r\n" ++ + "\r\n"); + const reader = stream.reader(); + + var response = try http.HttpResponseParser.init(allocator); + defer response.deinit(); + + try response.parseReader(reader); + return try DailyData.parse(allocator, response.body()); +} + +test "Parse a daily data block correctly with DailyDataSchema" { + const allocator = std.testing.allocator; + + const body = +\\ {"latitude":52.52,"longitude":13.419998,"generationtime_ms":0.4259347915649414, +\\ "utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation":"GMT", +\\ "elevation":38.0,"hourly_units": +\\ {"time":"iso8601","temperature_2m":"°C"}, +\\ "hourly":{"time":["2023-04-09T00:00","2023-04-09T01:00"],"temperature_2m":[4.3,3.6]}} +; + var stream = std.json.TokenStream.init(body); + const parsedData = try std.json.parse(DailyDataSchema, &stream, .{ + .allocator = allocator + }); + defer std.json.parseFree(DailyDataSchema, parsedData, .{ + .allocator = allocator + }); + + try expect( std.mem.eql(u8, parsedData.timezone_abbreviation, "GMT") ); + try expect( std.mem.eql(u8, parsedData.hourly.time[0], "2023-04-09T00:00") ); +} + +test "Get data properly and parse it (requires access to open-meteo)" { + const allocator = std.testing.allocator; + + var daily = try getDailyData(allocator); + defer daily.deinit(); + try expect( std.mem.eql(u8, daily.data.hourly_units.time, "iso8601") ); +} -- cgit v1.2.3