dwyl / dwyl/learn-zig

Wrap the C library libmagic in Zig and create an Elixir library via Zigler

Open
#31 5 comments 0 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
4
Forks
1
PR merge metrics
No merged PRs in 30d

Description

## 1) Zig library wrapping `libmagic`

You have `libmagic` installed on your system:

- macOS: `brew install libmagic`
- Ubuntu/Debian: `apt install libmagic-dev`

This time, a simple `@cImport` does the trick to use a system library. Building this library to produce a vendored static `.a` or `.so` is complicated. However, the opaque struct `magic_t` is accepted with no problem at all. Made things really easy for a change.

The code is interesting - as a beginner - because `libmagic` wants cpointers to sentinel terminated slices. So you need to juggle between a pointer to a sentinel `[*c]const u8` and `Zig`-slices `[]const u8`. This is used in the `Magic.from_path` method.

- Zig to C: use `std.mem.Allocator.dupeZ`
- C to Zig: use `std.mem.span`

```zig
//! By convention, root.zig is the root source file when making a library.
const std = @import("std");
const m = @cImport(@cInclude("magic.h"));

pub const Magic = struct {
cookie: m.magic_t,

pub fn init() !Magic {
const cookie = m.magic_open(m.MAGIC_MIME_TYPE) orelse return error.MagicOpenFailed;
return Magic{ .cookie = cookie };
}

pub fn deinit(self: *Magic) void {
m.magic_close(self.cookie);
}

pub fn load(self: *Magic) !void {
if (m.magic_load(self.cookie, null) != 0) {
self.deinit();
return error.MagicLoadFailed;
}
}

pub fn merror(self: *Magic) []const u8 {
const err = std.mem.span(m.magic_error(self.cookie));
return err;
}

pub fn from_path(self: *Magic, allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
const path_z = try allocator.dupeZ(u8, path);
defer allocator.free(path_z);

const res = m.magic_file(self.cookie, path_z.ptr);
if (res == null) {
return error.MagicFileFailed;
}
return std.mem.span(res);
}

pub fn from_buffer(self: *Magic, buffer: []const u8) ![]const u8 {
const res = m.magic_buffer(self.cookie, buffer.ptr, buffer.len);
if (res == null) {
return error.MagicBufferFailed;
}
return std.mem.span(res);
}

pub fn from_handle(self: *Magic, handle: i32) ![]const u8 {
const res = m.magic_descriptor(self.cookie, handle);
if (res == null) {
return error.MagicFdFailed;
}
return std.mem.span(res);
}
};
```

### Tests

`zig build test`

Test details

I have a "real" `.png` file, and the others are `.txt` files.

```zig
const std = @import("std");
const lib = @import("magic_zig");

const m = lib.Magic;

test "file buffer" {
const allocator = std.testing.allocator;
var magic = try m.init();
defer magic.deinit();

const file_contents = try std.fs.cwd().readFileAlloc(allocator, "src/main.zig", 1024 * 1024);
defer allocator.free(file_contents);
try magic.load();
const mime = try magic.from_buffer(file_contents);
try std.testing.expectEqualSlices(u8, "text/x-c", mime);
}

test "streamed buffer" {
const allocator = std.testing.allocator;
var magic = try m.init();
defer magic.deinit();

var file = try std.fs.cwd().openFile("src/main.zig", .{ .mode = .read_only });
defer file.close();

var read_buf: [1024]u8 = undefined;
var file_reader = file.reader(&read_buf);
const reader = &file_reader.interface;

var line = std.Io.Writer.Allocating.init(allocator);
defer line.deinit();

var file_as_list: std.ArrayList(u8) = .empty;

// read line by line
while (true) {
_ = reader.streamDelimiter(&line.writer, '\n') catch |err| {
if (err == error.EndOfStream) break else return err;
};
reader.toss(1); // consume the delimiter
try file_as_list.appendSlice(allocator, line.written());
line.clearRetainingCapacity();
}

try magic.load();
const streamed_content = try file_as_list.toOwnedSlice(allocator);
defer allocator.free(streamed_content);

const mime = try magic.from_buffer(streamed_content);
try std.testing.expectEqualSlices(u8, "text/x-c", mime);
}

test "file descriptor" {
var magic = try m.init();
defer magic.deinit();

var file = try std.fs.cwd().openFile("src/main.zig", .{ .mode = .read_only });
defer file.close();

try magic.load();
const mime = try magic.from_handle(file.handle);
try std.testing.expectEqualSlices(u8, "text/x-c", mime);
}

test "from paths" {
const allocator = std.testing.allocator;
var magic = try m.init();
defer magic.deinit();

try magic.load();

const paths = [_][]const u8{
"src/tests/icons8-globe-24.png",
"src/tests/t.png",
"src/tests/test.txt",
"src/tests/eggs-2-svgrepo-com.svg",
"src/tests/ex_module.ex",
"src/tests/test.json",
"src/tests/robots.txt.gz",
"README.md",
"src/tests/htmz.sql3",
"src/tests/simple.js",
};

const expected_mimes = [_][]const u8{
"image/png",
"text/plain",
"text/plain",
"image/svg+xml",
"text/plain",
"application/json",
"application/gzip",
"text/plain",
"application/vnd.sqlite3",
"application/javascript",
};

for (paths, 0..) |path, i| {
const mime = try magic.from_path(allocator, path);

try std.testing.expectEqualSlices(u8, expected_mimes[i], mime);
}
}

```

### Documentation

Example of docs generated by `zig build docs`and appended to github-pages:

Documentation setup in build.zig

```zig
// build.zig
// Documentation
const docs_step = b.step("docs", "Build zexplorer library docs");
const docs_obj = b.addObject(.{
.name = "zexplorer_docs",
.root_module = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
}),
});
const docs = docs_obj.getEmittedDocs();
docs_step.dependOn(&b.addInstallDirectory(.{
.source_dir = docs,
.install_dir = .prefix,
.install_subdir = "docs",
}).step);
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.