24 Commits

Author SHA1 Message Date
cec8d0e490 feat: init command. 2026-06-16 12:00:42 -04:00
c663f15618 fix: Fixed issue with buffer size. 2026-06-16 12:00:42 -04:00
f3a0dd842e refactor: Moved deps into root.zig. 2026-06-16 12:00:42 -04:00
f15d2e4297 feat: Added table viewer. 2026-06-16 12:00:42 -04:00
aedbeb28bd feat: list cmd. 2026-06-16 12:00:42 -04:00
336bd37613 feat: Restore db from file. 2026-06-16 12:00:42 -04:00
d00055aa3e feat(config): Added data path. 2026-06-16 12:00:42 -04:00
256382ce11 feat: accept config in Db 2026-06-16 12:00:41 -04:00
3277102b19 feat(age): accept multiple recipients. 2026-06-16 12:00:41 -04:00
20d4ad5c28 feat: Implemented basic db operation. 2026-06-16 12:00:41 -04:00
3fd3ca2b10 feat: Created own age wrapper. 2026-06-16 12:00:41 -04:00
0db97b714f feat: Added age-ffi. 2026-06-16 12:00:41 -04:00
41944af80c feat: zig-sqlite. 2026-06-16 12:00:41 -04:00
8d1b0ffa2d feat: added Config parsing. 2026-06-16 12:00:41 -04:00
ccaeda8f8f feat(comma): Added help method. 2026-06-16 12:00:40 -04:00
61ab6925b8 docs: Added AI Disclaimer to README.md. 2026-06-16 12:00:40 -04:00
b4952a4b2d feat: Added help output for commands. 2026-06-16 12:00:40 -04:00
33ca6525e2 feat(comma): Added enum value for unknown commands. 2026-06-16 12:00:40 -04:00
89ac19b246 feat: Migrated deps command. 2026-06-16 12:00:40 -04:00
5a3bc605c7 refactor: Broke comma into a separate package. 2026-06-16 12:00:40 -04:00
a0cbaaddf1 feat: Added command structure. 2026-06-16 12:00:39 -04:00
898a919145 wip: feat: Migrated version command to zig. 2026-06-16 12:00:39 -04:00
a7be0c719d feat: Created zig wrapper. 2026-06-16 12:00:39 -04:00
09df5639a7 feat: Added zig config. 2026-06-16 12:00:39 -04:00
81 changed files with 32173 additions and 0 deletions

3
.envrc
View File

@@ -1 +1,4 @@
use flake
ROOT="/home/spencer/github.com/envr-zig"
export PATH=".:${ROOT}/deps/zig:${ROOT}/deps/zls:$PATH"

7
.gitignore vendored
View File

@@ -1,11 +1,18 @@
# dev env
.direnv
/.env
# dependencies
deps
vendor
# docs
man
# build artifacts
.zig-cache
builds
envr
envr-go
result
zig-pkg

View File

@@ -3,6 +3,10 @@
Have you ever wanted to back up all your .env files in case your hard drive gets
nuked? `envr` makes it easier.
> [!CAUTION]
> The Zig community is quite anti-AI. Please read the [AI Disclaimer](#ai-disclaimer)
> before wasting your time.
`envr` is a binary application that tracks your `.env` files
in an encyrpted sqlite database. Changes can be effortlessly synced with
`envr sync`, and restored with `envr restore`.
@@ -132,3 +136,13 @@ This project is licensed under the [MIT License](./LICENSE).
For issues, feature requests, or questions, please
[open an issue](https://github.com/sbrow/envr/issues).
## AI Disclaimer
Unless noted here, you can be assured that I have personally written and reviewed
every line of code in this software.
- Many compiler errors that couldn't be solved with a quick google search were
solved by passing errors to AI and transcribing the suggestions.
- The "Pre-Zig" version of this readme was written by AI and then edited by me.
- The Go code was mostly written using opencode, and manually tested by me.

179
build.zig Normal file
View File

@@ -0,0 +1,179 @@
const std = @import("std");
// Although this function looks imperative, it does not perform the build
// directly and instead it mutates the build graph (`b`) that will be then
// executed by an external runner. The functions in `std.Build` implement a DSL
// for defining build steps and express dependencies between them, allowing the
// build runner to parallelize the build automatically (and the cache system to
// know when a step doesn't need to be re-run).
pub fn build(b: *std.Build) void {
// Standard target options allow 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 optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
// It's also possible to define more custom flags to toggle optional features
// of this build script using `b.option()`. All defined flags (including
// target and optimize options) will be listed when running `zig build --help`
// in this directory.
const comma = b.addModule("comma", .{
.root_source_file = b.path("src/comma.zig"),
.target = target,
});
const sqlite = b.dependency("sqlite", .{
.target = target,
.optimize = optimize,
});
// This creates a module, which represents a collection of source files alongside
// some compilation options, such as optimization mode and linked system libraries.
// Zig modules are the preferred way of making Zig code available to consumers.
// addModule defines a module that we intend to make available for importing
// to our consumers. We must give it a name because a Zig package can expose
// multiple modules and consumers will need to be able to specify which
// module they want to access.
const mod = b.addModule("envr", .{
// The root source file is the "entry point" of this module. Users of
// this module will only be able to access public declarations contained
// in this file, which means that if you have declarations that you
// intend to expose to consumers that were defined in other files part
// of this module, you will have to make sure to re-export them from
// the root file.
.root_source_file = b.path("src/root.zig"),
// Later on we'll use this module as the root module of a test executable
// which requires us to specify a target.
.target = target,
.imports = &.{
.{ .name = "comma", .module = comma },
},
});
mod.addImport("sqlite", sqlite.module("sqlite"));
// Here we define an executable. An executable needs to have a root module
// which needs to expose a `main` function. While we could add a main function
// to the module defined above, it's sometimes preferable to split business
// logic and the CLI into two separate modules.
//
// If your goal is to create a Zig library for others to use, consider if
// it might benefit from also exposing a CLI tool. A parser library for a
// data serialization format could also bundle a CLI syntax checker, for example.
//
// If instead your goal is to create an executable, consider if users might
// be interested in also being able to embed the core functionality of your
// program in their own executable in order to avoid the overhead involved in
// subprocessing your CLI tool.
//
// If neither case applies to you, feel free to delete the declaration you
// don't need and to put everything under a single module.
const exe = b.addExecutable(.{
.name = "envr",
.root_module = b.createModule(.{
// b.createModule defines a new module just like b.addModule but,
// unlike b.addModule, it does not expose the module to consumers of
// this package, which is why in this case we don't have to give it a name.
.root_source_file = b.path("src/main.zig"),
// Target and optimization levels must be explicitly wired in when
// defining an executable or library (in the root module), and you
// can also hardcode a specific target for an executable or library
// definition if desireable (e.g. firmware for embedded devices).
.target = target,
.optimize = optimize,
// List of modules available for import in source files part of the
// root module.
.imports = &.{
// Here "envr" is the name you will use in your source code to
// import this module (e.g. `@import("envr")`). The name is
// repeated because you are allowed to rename your imports, which
// can be extremely useful in case of collisions (which can happen
// importing modules from different packages).
.{ .name = "comma", .module = comma },
.{ .name = "envr", .module = mod },
},
}),
});
const version = b.option([]const u8, "version", "application version string") orelse "dev";
const options = b.addOptions();
options.addOption([]const u8, "version", version);
exe.root_module.addOptions("config", options);
// This declares intent for the executable to be installed into the
// install prefix when running `zig build` (i.e. when executing the default
// step). By default the install prefix is `zig-out/` but can be overridden
// by passing `--prefix` or `-p`.
b.installArtifact(exe);
// This creates a top level step. Top level steps have a name and can be
// invoked by name when running `zig build` (e.g. `zig build run`).
// This will evaluate the `run` step rather than the default step.
// For a top level step to actually do something, it must depend on other
// steps (e.g. a Run step, as we will see in a moment).
const run_step = b.step("run", "Run the app");
// This creates a RunArtifact step in the build graph. A RunArtifact step
// invokes an executable compiled by Zig. Steps will only be executed by the
// runner if invoked directly by the user (in the case of top level steps)
// or if another step depends on it, so it's up to you to define when and
// how this Run step will be executed. In our case we want to run it when
// the user runs `zig build run`, so we create a dependency link.
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
// By making the run step depend on the default step, it will be run from the
// installation directory rather than directly from within the cache directory.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
// Creates an executable that will run `test` blocks from the provided module.
// Here `mod` needs to define a target, which is why earlier we made sure to
// set the releative field.
const mod_tests = b.addTest(.{
.root_module = mod,
});
// A run step that will run the test executable.
const run_mod_tests = b.addRunArtifact(mod_tests);
// Creates an executable that will run `test` blocks from the executable's
// root module. Note that test executables only test one module at a time,
// hence why we have to create two separate ones.
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
// A run step that will run the second test executable.
const run_exe_tests = b.addRunArtifact(exe_tests);
// A top level step for running all tests. dependOn can be called multiple
// times and since the two run steps do not depend on one another, this will
// make the two of them run in parallel.
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
// Just like flags, top level steps are also listed in the `--help` menu.
//
// The Zig build system is entirely implemented in userland, which means
// that it cannot hook into private compiler APIs. All compilation work
// orchestrated by the build system will result in other Zig compiler
// subcommands being invoked with the right flags defined. You can observe
// these invocations when one fails (or you pass a flag to increase
// verbosity) to validate assumptions and diagnose problems.
//
// Lastly, the Zig build system is relatively simple and self-contained,
// and reading its source code will allow you to master it.
}

84
build.zig.zon Normal file
View File

@@ -0,0 +1,84 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .envr,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.3.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0xa89bf067266a3e10, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.16.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// .age = .{ .path = "zig-vendor/age-ffi/zig" },
.sqlite = .{ .path = "zig-vendor/zig-sqlite" },
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

View File

@@ -15,6 +15,7 @@ var (
var long bool
// versionCmd represents the version command
// Deprecated: Remove when Zig has the chance to emit help
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show envr's version",

View File

@@ -0,0 +1,21 @@
{
"db_path": "~/.envr/data.age",
"keys": [
{
"private": "~/.ssh/id_ed25519",
"public": "~/.ssh/id_ed25519.pub"
}
],
"scan": {
"matcher": "\\.env",
"exclude": [
"*\\.envrc",
"\\.local",
"node_modules",
"vendor"
],
"include": [
"~"
]
}
}

Binary file not shown.

Binary file not shown.

BIN
fixtures/example.db Normal file

Binary file not shown.

5
fixtures/hello-world.age Normal file
View File

@@ -0,0 +1,5 @@
age-encryption.org/v1
-> ssh-ed25519 Boe0UQ 2ngx7jSJ8/yuAzTgeiiCTYZRSkBCeJfaHTL0u7k6ziU
0XmEy0bOTeW1MF9ev32n4xISPDl9UQNHzEB0vsZHDuU
--- UV7IjWFCCg79Pf3T9vUWBxT4MhgeARWp6E+LK9tMy1g
u‡No2Zÿꥡé–Ý…++˜‡°ð¾ÓYÏóíð<C3AD>y:æ@' xP¾

1
fixtures/hello-world.txt Normal file
View File

@@ -0,0 +1 @@
Hello, World!

View File

@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCbll0MJper9prPwGn2wwikH3hTByL8tlzmhViuvfrryAAAAJCkxfzapMX8
2gAAAAtzc2gtZWQyNTUxOQAAACCbll0MJper9prPwGn2wwikH3hTByL8tlzmhViuvfrryA
AAAEDXQExhs89b3fjqJHkhuo9QX4JEjXiEC+vSnCAYc8OxcpuWXQwml6v2ms/AafbDCKQf
eFMHIvy2XOaFWK69+uvIAAAACnNwZW5jZXJAZncBAgM=
-----END OPENSSH PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJuWXQwml6v2ms/AafbDCKQfeFMHIvy2XOaFWK69+uvI spencer@fw

BIN
fixtures/single-file.db Normal file

Binary file not shown.

View File

@@ -93,6 +93,7 @@
nushell
go
gopls
sqlite
gotools
cobra-cli
@@ -103,8 +104,12 @@
unstable.ols
# Build tools
age
unstable.cargo
zip
opencode
# IDE
unstable.helix
typescript-language-server

226
src/Config.zig Normal file
View File

@@ -0,0 +1,226 @@
const std = @import("std");
db_path: []const u8 = "~/.envr/data.age",
/// Keys that are available for encryption
keys: []const SSHKeyPair = &.{
.from_pub_path("~/.ssh/id_ed25519.pub"),
},
/// Rules for how to match the scan command
scan: ScanConfig = .default,
// TODO: Allow incomplete pairs
pub const SSHKeyPair = struct {
private: []const u8,
public: []const u8,
/// Caller owns the returned memory
pub fn from_path(
gpa: std.mem.Allocator,
path: []const u8,
) error{OutOfMemory}!SSHKeyPair {
if (std.mem.eql(u8, std.fs.path.extension(path), ".pub")) {
return from_pub_path(path);
} else {
return .{
.public = try std.mem.concat(gpa, u8, &.{ path, ".pub" }),
.private = path,
};
}
}
pub fn from_pub_path(path: []const u8) SSHKeyPair {
std.debug.assert(std.mem.eql(u8, std.fs.path.extension(path), ".pub"));
return .{
.public = path,
.private = path[0 .. path.len - 4],
};
}
};
/// Configuration for the scan command
pub const ScanConfig = struct {
/// the file extension to look for
matcher: []const u8,
/// Glob patterns to ignore
exclude: []const []const u8,
/// paths to search in
include: []const []const u8,
const default: @This() = .{
.matcher = "\\.env",
.exclude = &.{
"*\\.envrc",
"\\.local",
"node_modules",
"vendor",
},
.include = &.{"~"},
};
};
/// Load the Config from the file at path
/// TODO: Use a concrete error set
pub fn load(
io: std.Io,
gpa: std.mem.Allocator,
path: []const u8,
) !std.json.Parsed(@This()) {
var file = try std.Io.Dir.cwd().openFile(
io,
path,
.{ .mode = .read_only },
);
defer file.close(io);
var buffer: [4096]u8 = undefined;
var reader = file.reader(io, &buffer);
var json_reader: std.json.Reader = .init(gpa, &reader.interface);
defer json_reader.deinit();
return try std.json.parseFromTokenSource(
@This(),
gpa,
&json_reader,
.{},
);
}
/// Save the config to the given file
pub fn save(
self: *@This(),
io: std.Io,
dir: std.Io.Dir,
path: []const u8,
) !void {
// TODO: Remove dependence on string?
var string: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer string.deinit();
try string.writer.print(
"{f}",
.{std.json.fmt(self, .{ .whitespace = .indent_2 })},
);
var file = try dir.createFile(io, path, .{ .truncate = true });
defer file.close(io);
try file.writeStreamingAll(io, string.written());
}
test "loading the default config from disk matches expected values" {
const gpa = std.testing.allocator;
const parsed = try load(std.testing.io, gpa, "./fixtures/default_config.json");
defer parsed.deinit();
const got = parsed.value;
try std.testing.expectEqualDeep(got.scan, ScanConfig.default);
}
test "saving to a new file upserts the file" {
const io = std.testing.io;
var cfg: @This() = .{};
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var dir = tmp.dir;
try std.testing.expectError(
error.FileNotFound,
dir.statFile(io, "config.json", .{}),
);
try cfg.save(io, dir, "config.json");
const contents = try dir.readFileAlloc(
io,
"config.json",
std.testing.allocator,
.unlimited,
);
defer std.testing.allocator.free(contents);
const want =
\\{
\\ "db_path": "~/.envr/data.age",
\\ "keys": [
\\ {
\\ "private": "~/.ssh/id_ed25519",
\\ "public": "~/.ssh/id_ed25519.pub"
\\ }
\\ ],
\\ "scan": {
\\ "matcher": "\\.env",
\\ "exclude": [
\\ "*\\.envrc",
\\ "\\.local",
\\ "node_modules",
\\ "vendor"
\\ ],
\\ "include": [
\\ "~"
\\ ]
\\ }
\\}
;
try std.testing.expectEqualSlices(u8, want, contents);
}
test "saving to an existing file updates the file" {
const io = std.testing.io;
var cfg: @This() = .{};
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var dir = tmp.dir;
try dir.writeFile(io, .{ .sub_path = "config.json", .data = "{}" });
_ = try dir.statFile(io, "config.json", .{});
try cfg.save(io, dir, "config.json");
const contents = try dir.readFileAlloc(
io,
"config.json",
std.testing.allocator,
.unlimited,
);
defer std.testing.allocator.free(contents);
const want =
\\{
\\ "db_path": "~/.envr/data.age",
\\ "keys": [
\\ {
\\ "private": "~/.ssh/id_ed25519",
\\ "public": "~/.ssh/id_ed25519.pub"
\\ }
\\ ],
\\ "scan": {
\\ "matcher": "\\.env",
\\ "exclude": [
\\ "*\\.envrc",
\\ "\\.local",
\\ "node_modules",
\\ "vendor"
\\ ],
\\ "include": [
\\ "~"
\\ ]
\\ }
\\}
;
try std.testing.expectEqualSlices(u8, want, contents);
}

488
src/Db.zig Normal file
View File

@@ -0,0 +1,488 @@
//! Db interacts with an age encrypted sqlite database.
//!
const std = @import("std");
const sqlite = @import("sqlite");
const age = @import("age.zig");
const Config = @import("Config.zig");
/// controls the keys and filepaths used for saving
opts: OpenOptions,
/// The underlying data store.
sql_db: sqlite.Db,
/// Set to true whenever the data updates. If false when close() is called,
/// the database will be closed without saving
changed: bool = false,
/// Decrypts the database into a temporary file and opens it in memory
// FIXME: Test me with real file
pub fn open(
io: std.Io,
gpa: std.mem.Allocator,
opts: OpenOptions,
) !@This() {
// FIXME: cheating here
const db_path = try std.fs.path.join(gpa, &.{
opts.home,
opts.config.db_path[2..],
});
defer gpa.free(db_path);
// const tmp_dir = try std.Io.Dir.cwd().openDir(io, tmp, .{});
// defer tmp_dir.deleteFile(io, "envr.db");
const tmp_db_path = try std.fs.path.joinZ(gpa, &.{ opts.tmp, "envr.db" });
defer gpa.free(tmp_db_path);
if (db_exists(io, db_path)) {
// TODO: Use std.MultiArrayList? Had json issues
{
var private_keys: std.ArrayList([]const u8) = try .initCapacity(
gpa,
opts.config.keys.len,
);
defer private_keys.deinit(gpa);
for (opts.config.keys) |key| {
// FIXME: cheating here
if (std.mem.startsWith(u8, key.private, "~/")) {
const key_path = try std.fs.path.join(gpa, &.{
opts.home,
key.private[2..],
});
private_keys.appendAssumeCapacity(key_path);
// defer gpa.free(key_path);
} else {
private_keys.appendAssumeCapacity(key.private);
}
}
// TODO: Pass key(s) from Config
try age.decrypt(io, gpa, private_keys.items, db_path, tmp_db_path);
for (opts.config.keys, 0..) |key, i| {
if (std.mem.startsWith(u8, key.private, "~/")) {
gpa.free(private_keys.items[i]);
}
}
}
}
return open_decrypted(opts, tmp_db_path);
}
const OpenOptions = struct {
config: Config = .{},
/// The path to the home directory
home: []const u8 = "~/",
/// The path to the /tmp directory
// FIXME: Support windows
tmp: []const u8 = "/tmp",
};
/// Create a new instance of the database
fn open_decrypted(opts: OpenOptions, tmp_db_path: [:0]const u8) !@This() {
var db = try sqlite.Db.init(.{
.mode = .{ .File = tmp_db_path },
.open_flags = .{
.write = true,
.create = true,
},
.threading_mode = .MultiThread,
});
try db.exec(
\\create table if not exists envr_env_files (
\\ path text primary key not null
\\, remotes text -- JSON
\\, sha256 text not null
\\, contents text not null
\\)
, .{}, .{});
return .{
.sql_db = db,
.opts = opts,
};
}
/// Returns true if a file exists at ~/.envr/data.age
fn db_exists(io: std.Io, path: []const u8) bool {
if (std.Io.Dir.cwd().access(io, path, .{ .read = true })) {
return true;
} else |_| {
return false;
}
}
// TODO: Finish
// pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir {
// var random_bytes: [TmpDir.random_bytes_count]u8 = undefined;
// std.crypto.random.bytes(&random_bytes);
// var sub_path: [TmpDir.sub_path_len]u8 = undefined;
// _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
// }
//
// const TmpDir = struct {};
/// Close the database
/// FIXME: Test me with data but no changes
/// FIXME: Test me with data and changes
pub fn close(
self: *@This(),
io: std.Io,
gpa: std.mem.Allocator,
) !void {
defer self.sql_db.deinit();
if (self.changed) {
const tmp_db_path = try std.fs.path.join(gpa, &.{ self.opts.tmp, "envr.db" });
defer gpa.free(tmp_db_path);
try self.sql_db.exec("VACUUM INTO ?", .{}, .{tmp_db_path});
const db_path = try std.fs.path.join(gpa, &.{ self.opts.home, ".envr", "data.age" });
defer gpa.free(db_path);
{
// TODO: Use std.MultiArrayList? Had json issues
var public_keys: std.ArrayList([]const u8) = try .initCapacity(
gpa,
self.opts.config.keys.len,
);
defer public_keys.deinit(gpa);
for (self.opts.config.keys) |key| {
public_keys.appendAssumeCapacity(key.private);
}
try age.encrypt(io, gpa, public_keys.items, tmp_db_path, db_path);
}
self.changed = false;
}
}
/// Returns a list of all the .env files present in the database.
/// The caller is responsible for freeing memory
pub fn list(self: *@This(), gpa: std.mem.Allocator) ![]EnvFile {
var stmt = try self.sql_db.prepare(
"select path, remotes, sha256, contents from envr_env_files",
);
defer stmt.deinit();
return stmt.all(EnvFile, gpa, .{}, .{});
}
pub const EnvFile = struct {
// TODO: Should use file_name in the struct and derive from the path.
path: []const u8,
// /// dir is derived from Path, and is not stored in the database.
// dir: []const u8,
/// JSON encoded list of strings
remotes: []const u8,
sha256: []const u8,
contents: []const u8,
pub fn deinit(self: *EnvFile, alloc: std.mem.Allocator) void {
alloc.free(self.path);
alloc.free(self.remotes);
alloc.free(self.sha256);
alloc.free(self.contents);
}
};
test {
std.testing.refAllDecls(@import("age.zig"));
}
test "simple database can be opened" {
var db = try sqlite.Db.init(.{
.mode = sqlite.Db.Mode{ .File = "./fixtures/example.db" },
.open_flags = .{
.write = false,
.create = false,
},
.threading_mode = .MultiThread,
});
var stmt = try db.prepare("SELECT * FROM hello");
defer stmt.deinit();
const alloc = std.testing.allocator;
if (try stmt.oneAlloc(struct { text: []const u8 }, alloc, .{}, .{})) |got| {
defer alloc.free(got.text);
try std.testing.expectEqualSlices(u8, "world!", got.text);
} else {
return error.TestUnexpectedResult;
}
}
test "encrypted database can be opened" {
const io = std.testing.io;
const gpa = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const dir_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(dir_path);
const decrypted_path = try std.fs.path.joinZ(gpa, &.{ dir_path, "example.db" });
defer gpa.free(decrypted_path);
try age.decrypt(
io,
gpa,
&.{"./fixtures/insecure-test-key"},
"./fixtures/encrypted-example.db.age",
decrypted_path,
);
var db = try sqlite.Db.init(.{
.mode = sqlite.Db.Mode{ .File = decrypted_path },
.open_flags = .{
.write = false,
.create = false,
},
.threading_mode = .MultiThread,
});
var stmt = try db.prepare("SELECT * FROM hello");
defer stmt.deinit();
const alloc = std.testing.allocator;
if (try stmt.oneAlloc(struct { text: []const u8 }, alloc, .{}, .{})) |got| {
defer alloc.free(got.text);
try std.testing.expectEqualSlices(u8, "world!", got.text);
} else {
return error.TestUnexpectedResult;
}
}
test "Closing a fresh database does not create a file" {
const io = std.testing.io;
const gpa = std.testing.allocator;
var tmp_dir = std.testing.tmpDir(.{});
defer tmp_dir.cleanup();
try tmp_dir.dir.createDir(io, "home", .default_dir);
try tmp_dir.dir.createDir(io, "tmp", .default_dir);
const tmp_dir_path = try tmp_dir.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(tmp_dir_path);
const home = try std.fs.path.join(gpa, &.{ tmp_dir_path, "home" });
defer gpa.free(home);
const tmp = try std.fs.path.join(gpa, &.{ tmp_dir_path, "tmp" });
defer gpa.free(tmp);
// TODO: Pass testing keys
var db: @This() = try .open(io, gpa, .{ .home = home, .tmp = tmp });
// TODO: Get rid of direct access
const db_path = try std.fs.path.join(gpa, &.{ home, ".envr", "data.age" });
defer gpa.free(db_path);
try std.testing.expectError(
error.FileNotFound,
tmp_dir.dir.access(io, db_path, .{ .read = true }),
);
try db.close(io, gpa);
try std.testing.expectError(
error.FileNotFound,
tmp_dir.dir.access(io, db_path, .{ .read = true }),
);
}
test "single-file.db has envr_env_files table" {
const io = std.testing.io;
const gpa = std.testing.allocator;
const dir_path = try std.Io.Dir.cwd().realPathFileAlloc(io, ".", gpa);
defer gpa.free(dir_path);
const path = try std.fs.path.joinZ(
gpa,
&.{ dir_path, "fixtures", "single-file.db" },
);
defer gpa.free(path);
var db = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{
.write = false,
.create = false,
},
.threading_mode = .MultiThread,
});
var diags: sqlite.Diagnostics = .{};
var stmt = db.prepareDynamicWithDiags(
"select name from sqlite_master where type='table'",
.{ .diags = &diags },
) catch |err| {
std.log.err(
"unable to prepare statement, got error {}. diagnostics: {f}",
.{ err, diags },
);
return err;
};
defer stmt.deinit();
const tables = (try stmt.oneAlloc(
[]const u8,
gpa,
.{ .diags = &diags },
.{},
)).?;
defer gpa.free(tables);
try std.testing.expectEqualSlices(u8, "envr_env_files", tables);
}
// test "raw restore works" {
// const io = std.testing.io;
// const gpa = std.testing.allocator;
// var db = try sqlite.Db.init(.{
// .mode = .Memory,
// .open_flags = .{
// .write = true,
// .create = true,
// },
// .threading_mode = .MultiThread,
// });
// try db.exec(
// \\create table envr_env_files (
// \\ path text primary key not null
// \\, remotes text -- JSON
// \\, sha256 text not null
// \\, contents text not null
// \\)
// , .{}, .{});
// const dir_path = try std.Io.Dir.cwd().realPathFileAlloc(io, ".", gpa);
// defer gpa.free(dir_path);
// const path = try std.fs.path.join(
// gpa,
// &.{ dir_path, "fixtures", "single-file.db" },
// );
// defer gpa.free(path);
// std.debug.print("path: {s}\n", .{path});
// try db.exec(
// "ATTACH DATABASE ? AS source",
// .{},
// .{path},
// );
// defer db.exec("DETACH DATABASE source", .{}, .{}) catch unreachable;
// var diags: sqlite.Diagnostics = .{};
// db.exec(
// "INSERT INTO main.envr_env_files SELECT * FROM source.envr_env_files",
// .{ .diags = &diags },
// .{},
// ) catch |err| {
// std.log.err(
// "unable to prepare statement, got error {}. diagnostics: {f}",
// .{ err, diags },
// );
// return err;
// };
// }
// test "Closing a modified database does create a file" {}
test "list displays the database's keys" {
const io = std.testing.io;
const gpa = std.testing.allocator;
var tmp_dir = std.testing.tmpDir(.{});
defer tmp_dir.cleanup();
try tmp_dir.dir.createDir(io, "home", .default_dir);
try tmp_dir.dir.createDir(io, "home/.envr", .default_dir);
try tmp_dir.dir.createDir(io, "tmp", .default_dir);
const tmp_dir_path = try tmp_dir.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(tmp_dir_path);
const home = try std.fs.path.join(gpa, &.{ tmp_dir_path, "home" });
defer gpa.free(home);
const tmp = try std.fs.path.join(gpa, &.{ tmp_dir_path, "tmp" });
defer gpa.free(tmp);
// TODO: Get rid of direct access
const db_path = try std.fs.path.join(gpa, &.{ home, ".envr", "data.age" });
defer gpa.free(db_path);
try std.Io.Dir.cwd().copyFile(
"fixtures/encrypted-single-file.db.age",
tmp_dir.dir,
"home/.envr/data.age",
io,
.{},
);
// Asserts file existence
try tmp_dir.dir.access(io, db_path, .{ .read = true });
// TODO: Pass testing keys
const config: Config = .{
.keys = &.{.from_pub_path("fixtures/insecure-test-key.pub")},
};
var db: @This() = try .open(io, gpa, .{
.config = config,
.home = home,
.tmp = tmp,
});
const env_files = try db.list(gpa);
defer gpa.free(env_files);
try std.testing.expectEqual(1, env_files.len);
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
try std.testing.expectEqual(1, env_files.len);
for (env_files) |*file| {
defer file.deinit(gpa);
try std.testing.expectEqualSlices(
u8,
"~/project/.env.example",
file.path,
);
try std.testing.expectEqualSlices(
u8,
"API_KEY=\\\"sk_my_api_key\\\"\\nAPP_ENV=testing",
file.contents,
);
try std.testing.expectEqualSlices(
u8,
"[\"git@github.com:user/project.git\"]",
file.remotes,
);
hasher.update(file.contents);
const hash = hasher.finalResult();
try std.testing.expectEqualStrings(&std.fmt.bytesToHex(&hash, .lower), file.sha256);
}
try db.close(io, gpa);
}

153
src/age.zig Normal file
View File

@@ -0,0 +1,153 @@
const std = @import("std");
/// Decrypts the file into output path
pub fn decrypt(
io: std.Io,
gpa: std.mem.Allocator,
private_keys: []const []const u8,
input_path: []const u8,
output_path: []const u8,
) !void {
// TODO: use raw array?
var argv: std.ArrayList([]const u8) = try .initCapacity(gpa, 2 + (2 * private_keys.len) + 3);
defer argv.deinit(gpa);
argv.appendAssumeCapacity("age");
argv.appendAssumeCapacity("-d");
for (private_keys) |key| {
argv.appendAssumeCapacity("-i");
argv.appendAssumeCapacity(key);
}
argv.appendAssumeCapacity("-o");
argv.appendAssumeCapacity(output_path);
argv.appendAssumeCapacity(input_path);
const result = try std.process.run(gpa, io, .{
.argv = argv.items,
});
defer gpa.free(result.stderr);
defer gpa.free(result.stdout);
if (result.stdout.len > 0) {
std.debug.print("stdout: \"{s}\"\n", .{result.stdout});
unreachable;
}
if (result.stderr.len > 0) {
std.debug.print("stderr: \"{s}\"\n", .{result.stderr});
unreachable;
}
}
/// Encrypts the file into output path
pub fn encrypt(
io: std.Io,
gpa: std.mem.Allocator,
// TODO: Accept multiple keys
public_keys: []const []const u8,
input_path: []const u8,
output_path: []const u8,
) !void {
var argv: std.ArrayList([]const u8) = try .initCapacity(gpa, 2 + (2 * public_keys.len) + 3);
defer argv.deinit(gpa);
argv.appendAssumeCapacity("age");
argv.appendAssumeCapacity("-e");
for (public_keys) |key| {
argv.appendAssumeCapacity("-R");
argv.appendAssumeCapacity(key);
}
argv.appendAssumeCapacity("-o");
argv.appendAssumeCapacity(output_path);
argv.appendAssumeCapacity(input_path);
const result = try std.process.run(gpa, io, .{
.argv = argv.items,
});
defer gpa.free(result.stderr);
defer gpa.free(result.stdout);
if (result.stdout.len > 0) {
std.debug.print("stdout: \"{s}\"\n", .{result.stdout});
unreachable;
}
if (result.stderr.len > 0) {
std.debug.print("stderr: \"{s}\"\n", .{result.stderr});
unreachable;
}
}
test "sample file can be decrypted" {
const io = std.testing.io;
const gpa = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const dir_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(dir_path);
const output_path = try std.fs.path.join(gpa, &.{ dir_path, "got.txt" });
defer gpa.free(output_path);
try decrypt(
io,
gpa,
&.{"./fixtures/insecure-test-key"},
"./fixtures/hello-world.age",
output_path,
);
const contents = try tmp.dir.readFileAlloc(io, output_path, gpa, .unlimited);
defer gpa.free(contents);
try std.testing.expectEqualSlices(u8, "Hello, World!\n", contents);
}
test "sample file can be encrypted" {
const io = std.testing.io;
const gpa = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const dir_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(dir_path);
const output_path = try std.fs.path.join(gpa, &.{ dir_path, "hello-world.age" });
defer gpa.free(output_path);
try encrypt(
io,
gpa,
&.{"./fixtures/insecure-test-key.pub"},
"./fixtures/hello-world.txt",
output_path,
);
const got = try tmp.dir.readFileAlloc(io, output_path, gpa, .unlimited);
defer gpa.free(got);
const want = try std.Io.Dir.cwd().readFileAlloc(
io,
"./fixtures/hello-world.age",
gpa,
.unlimited,
);
defer gpa.free(want);
const contents = try tmp.dir.readFileAlloc(io, output_path, gpa, .unlimited);
defer gpa.free(contents);
try std.testing.expectEqual(want.len, got.len);
// FIXME: Test that decrypted file contents match
// try std.testing.expectEqualSlices(u8, "Hello, World!\n", decrypted_contents);
}

146
src/comma.zig Normal file
View File

@@ -0,0 +1,146 @@
//! By convention, root.zig is the root source file when making a package.
const std = @import("std");
const Io = std.Io;
pub const Command = struct {
name: []const u8,
short: ?[]const u8 = null,
long: ?[]const u8 = null,
subcommands: []const Command = &.{},
examples: [][]const u8 = &.{},
/// The enum type of the command
Type: type,
/// The type of struct that holds the Commands's flags and arguments
// Params: type,
pub fn new(cmd: CommandOptions) Command {
const subcommands: [cmd.subcommands.len]Command = blk: {
var result: [cmd.subcommands.len]Command = undefined;
inline for (cmd.subcommands, 0..) |sub, idx| {
result[idx] = new(sub);
}
break :blk result;
};
return .{
.name = cmd.name,
.short = cmd.short,
.long = cmd.long,
.subcommands = &subcommands,
.Type = cmd.as_enum(),
};
}
pub fn parse(comptime self: @This(), args: []const []const u8) self.Type {
if (args.len == 0) {
return @enumFromInt(0);
}
const target = args[0];
inline for (self.subcommands, 1..) |cmd, idx| {
if (std.mem.eql(u8, target, cmd.name)) {
return @enumFromInt(idx);
}
}
return @enumFromInt(self.subcommands.len + 1);
}
/// Used for indentation when printing command help
const tab = " ";
/// Print usage information to the console.
pub fn help(self: @This(), w: *Io.Writer) !void {
defer w.flush() catch {};
if (self.long) |long| {
try w.print("{s}\n\n", .{long});
}
try w.print("Usage:\n{s}{s}\n", .{ tab, self.name });
if (self.subcommands.len > 0) {
try w.print("\nAvailable Commands:\n", .{});
var max_width: u8 = 0;
inline for (self.subcommands) |cmd| {
max_width = @max(max_width, cmd.name.len);
}
// Print short command description
inline for (self.subcommands) |cmd| {
try w.print(
"{s}{s}",
.{
tab,
cmd.name,
},
);
for (0..(max_width - cmd.name.len)) |_| {
try w.print(" ", .{});
}
try w.print(
" {s}\n",
.{
cmd.short orelse "",
},
);
}
try w.print("\n", .{});
}
// TODO: Print flags
// TODO: Print arguments
if (self.subcommands.len > 0) {
try w.print(
"Use \"{s} [command] --help\" for more information about a command.",
.{self.name},
);
}
}
};
pub const ParseError = error{
InvalidType,
};
const CommandOptions = struct {
name: []const u8,
short: ?[]const u8 = null,
long: ?[]const u8 = null,
subcommands: []const CommandOptions = &[0]CommandOptions{},
fn as_enum(self: @This()) type {
var field_names: [self.subcommands.len + 2][]const u8 = undefined;
var field_values: [self.subcommands.len + 2]u32 = undefined;
field_names[0] = self.name;
field_values[0] = 0;
inline for (self.subcommands, 1..) |cmd, idx| {
field_names[idx] = cmd.name;
field_values[idx] = idx;
}
field_names[self.subcommands.len + 1] = "unknown";
field_values[self.subcommands.len + 1] = self.subcommands.len + 1;
return @Enum(
u32,
.exhaustive,
&field_names,
&field_values,
);
}
};
// /// parses the args into params
// pub fn params(cmd: Command, args: [][]const u8) cmd.Params {
// }

149
src/main.zig Normal file
View File

@@ -0,0 +1,149 @@
const std = @import("std");
const Io = std.Io;
const config = @import("config");
const comma = @import("comma");
const envr = @import("envr");
const goBinary = "envr-go";
pub fn main(init: std.process.Init) !void {
// This is appropriate for anything that lives as long as the process.
const arena: std.mem.Allocator = init.arena.allocator();
const args = try init.minimal.args.toSlice(arena);
try run(init.environ_map, init.io, arena, args);
}
/// Attempt to run the requested command.
fn run(
environ_map: *std.process.Environ.Map,
io: Io,
arena: std.mem.Allocator,
args: []const [:0]const u8,
) !void {
const page_size = std.heap.pageSize();
const cmd = envr.root.parse(args[1..]);
switch (cmd) {
.envr => {
var stdout_buffer: [page_size]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
return envr.root.help(stdout_writer);
},
.deps => {
var stdout_buffer: [1024]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
return envr.deps(
io,
stdout_writer,
environ_map.get("PATH").?,
);
},
.init => {
var stdout_buffer: [1024]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
try envr.init_cmd(
io,
arena,
stdout_writer,
environ_map.get("HOME").?,
.{
// TODO: Actually parse this
.force = true,
},
);
},
.list => {
var stdout_buffer: [page_size]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
return envr.list(
io,
arena,
stdout_writer,
environ_map.get("HOME").?,
// TODO: Don't hardcode this?
"/tmp",
);
},
.version => {
var stdout_buffer: [1024]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
return version(stdout_writer);
},
.unknown => {
return fallback_to_go(io, arena, args);
},
}
}
fn version(writer: *Io.Writer) !void {
try writer.print("{s}\n", .{config.version});
try writer.flush();
}
fn fallback_to_go(
io: Io,
arena: std.mem.Allocator,
args: []const [:0]const u8,
) std.process.ReplaceError {
// Remap args
var childArgs = try std.ArrayList([]const u8).initCapacity(arena, args.len);
childArgs.appendAssumeCapacity(goBinary);
for (args[1..]) |arg| {
childArgs.appendAssumeCapacity(arg);
}
return std.process.replace(io, .{ .argv = childArgs.items });
}
test "simple test" {
const gpa = std.testing.allocator;
var alist: std.ArrayList(i32) = .empty;
defer alist.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
try alist.append(gpa, 42);
try std.testing.expectEqual(@as(i32, 42), alist.pop());
}
test "fuzz example" {
try std.testing.fuzz({}, testOne, .{});
}
fn testOne(context: void, smith: *std.testing.Smith) !void {
_ = context;
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
const gpa = std.testing.allocator;
var alist: std.ArrayList(u8) = .empty;
defer alist.deinit(gpa);
while (!smith.eos()) switch (smith.value(enum { add_data, dup_data })) {
.add_data => {
const slice = try alist.addManyAsSlice(gpa, smith.value(u4));
smith.bytes(slice);
},
.dup_data => {
if (alist.items.len == 0) continue;
if (alist.items.len > std.math.maxInt(u32)) return error.SkipZigTest;
const len = smith.valueRangeAtMost(u32, 1, @min(32, alist.items.len));
const off = smith.valueRangeAtMost(u32, 0, @intCast(alist.items.len - len));
try alist.appendSlice(gpa, alist.items[off..][0..len]);
try std.testing.expectEqualSlices(
u8,
alist.items[off..][0..len],
alist.items[alist.items.len - len ..],
);
},
};
}

423
src/root.zig Normal file
View File

@@ -0,0 +1,423 @@
//! By convention, root.zig is the root source file when making a package.
const std = @import("std");
const Io = std.Io;
const Command = @import("comma").Command;
const Config = @import("Config.zig");
const Db = @import("Db.zig");
const tabula = @import("./tabula.zig");
pub const root: Command = .new(.{
.name = "envr",
.short = "Manage your .env files.",
.long =
\\envr keeps your .env synced to a local, age encrypted database.
\\It is a safe and eay way to gather all your .env files in one place where they can
\\easily be backed by another tool such as restic or git.
\\All your data is stored in ~/data.age
\\
\\Getting started is easy:
\\
\\1. Create your configuration file and set up encrypted storage:
\\
\\> envr init
\\
\\2. Scan for existing .env files:
\\
\\> envr scan
\\
\\Select the files you want to back up from the interactive list.
\\
\\3. Verify that it worked:
\\
\\> envr list
\\
\\4. After changing any of your .env files, update the backup with:
\\
\\> envr sync
\\
\\5. If you lose a repository, after re-cloning the repo into the same path it was
\\at before, restore your backup with:
\\
\\> envr restore <path to repository> .env
,
.subcommands = &.{
.{
.name = "deps",
.short = "Check for missing binaries",
.long =
\\envr relies on external binaries for certain functionality.
\\
\\ The deps command reports which binaries are available and which are not."
,
},
.{
.name = "init",
.short = "Set up envr",
.long =
\\The init command generates your initial config and saves it to
\\~/.envr/config in JSON format.
\\
\\During setup, you will be prompted to select one or more ssh keys with which to
\\encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
\\key somewhere, otherwise your data could be lost forever.
,
//.flags = struct { force: bool }
},
.{
.name = "list",
.short = "View your tracked files",
},
.{
.name = "version",
.short = "Show envr's version",
},
},
});
// Display dependency statuses
pub fn deps(
io: Io,
writer: *Io.Writer,
path: []const u8,
) !void {
const feats: Features = try .scan(io, path);
// FIXME: Draw as a table
try writer.print("features: {}", .{feats});
try writer.flush();
}
const Features = packed struct {
git: bool = false,
fd: bool = false,
const all_features: Features = .{
.git = true,
.fd = true,
};
/// Scans your PATH variable for programs.
pub fn scan(io: Io, path: []const u8) !@This() {
var feats: Features = .{};
var dirs = std.mem.splitScalar(u8, path, std.fs.path.delimiter);
loop: while (dirs.next()) |dir| {
const dirt = Io.Dir.openDir(Io.Dir.cwd(), io, dir, .{ .follow_symlinks = true, .iterate = true }) catch continue;
defer dirt.close(io);
var dir_paths = dirt.iterate();
while (try dir_paths.next(io)) |file| {
// FIXME: Check if executable
if (std.mem.eql(u8, std.fs.path.basename(file.name), "git")) {
feats.git = true;
if (feats == Features.all_features) {
break :loop;
}
}
if (std.mem.eql(u8, std.fs.path.basename(file.name), "fd")) {
feats.fd = true;
if (feats == Features.all_features) {
break :loop;
}
}
}
}
return feats;
}
};
pub fn init_cmd(
io: Io,
arena: std.mem.Allocator,
out: *std.Io.Writer,
home: []const u8,
flags: struct { force: bool },
) !void {
defer out.flush() catch unreachable;
// TODO: Don't hardcode
const cfgPath = try std.fs.path.join(arena, &.{ home, ".envr", "config.json" });
defer arena.free(cfgPath);
if (flags.force or !file_exists(io, cfgPath)) {
const keys = try select_ssh_keys(io, arena, home, out);
// defer {
// for (keys) |*key| {
// arena.destroy(key);
// }
// arena.free(&keys);
// }
// const cfg: Config = .{ .keys = keys };
// TODO: How to handle this error?
// try cfg.save(io, cfgPath);
try out.print(
"Config initialized with {} SSH key(s). You are ready to use envr.\n",
.{keys.len},
);
} else {
try out.writeAll(
\\You have already initialized envr.
\\Run again with the --force flag if you want to reinitialize.
\\
,
);
}
}
/// Returns true if the file exists
fn file_exists(io: std.Io, path: []const u8) bool {
if (std.Io.Dir.cwd().access(io, path, .{ .read = true })) {
return true;
} else |_| {
return false;
}
}
/// Returns a list of keys that the user has selected to add to their config.
/// Caller owns the returned memory
// TODO: Write a test for this
fn select_ssh_keys(
io: std.Io,
alloc: std.mem.Allocator,
home_path: []const u8,
out: *std.Io.Writer,
) ![]Config.SSHKeyPair {
const ssh_path = try std.fs.path.join(alloc, &.{ home_path, ".ssh" });
defer alloc.free(ssh_path);
// TODO: Arbitrary capacity chosen
var keys: std.ArrayList(Config.SSHKeyPair) = try .initCapacity(alloc, 3);
{
const ssh_dir = try std.Io.Dir.cwd().openDir(io, ssh_path, .{ .iterate = true });
defer ssh_dir.close(io);
var itr = ssh_dir.iterate();
const expect1 =
\\-----BEGIN OPENSSH PRIVATE KEY-----
\\
;
const expect2 =
\\-----BEGIN RSA PRIVATE KEY-----
\\
;
var buf: [expect1.len]u8 = undefined;
while (try itr.next(io)) |entry| {
switch (entry.kind) {
.file => {
var file = try ssh_dir.openFile(io, entry.name, .{});
_ = try file.readPositionalAll(io, &buf, 0);
// TODO: Faster to use hash or something?
if ( // zig fmt: off
std.mem.eql(u8, expect1, &buf) or
std.mem.eql(u8, expect2, buf[0..expect2.len])
) { // zig fmt: on
// File is a private ssh key
const full_path = try ssh_dir.realPathFileAlloc(
io,
entry.name,
alloc,
);
try keys.append(alloc, try .from_path(alloc, full_path));
}
},
.sym_link => {
// TODO: Handle symlinks
},
.block_device,
.character_device,
.directory,
.named_pipe,
.unix_domain_socket,
.whiteout,
.door,
.event_port,
.unknown,
=> continue,
}
}
}
for (keys.items, 1..) |key, n| {
try out.print("{d}. {s}\n", .{ n, key.private });
}
try out.writeAll(
"\nPlease enter the number(s) of SSH keys you'd like to use for encryption:\n> ",
);
try out.flush();
defer out.writeAll("\n\n") catch unreachable;
// TODO: ask user for number(s) to use.
// TODO: confirm with a y/n prompt
// TODO: only return selected keys
return keys.toOwnedSlice(alloc);
}
pub fn list(
io: Io,
arena: std.mem.Allocator,
out: *std.Io.Writer,
home: []const u8,
tmp: []const u8,
) !void {
// TODO: Don't hardcode
const cfgPath = try std.fs.path.join(arena, &.{ home, ".envr", "config.json" });
defer arena.free(cfgPath);
var cfg = (try Config.load(io, arena, cfgPath));
defer cfg.deinit();
var db: Db = try .open(io, arena, .{
.config = cfg.value,
.home = home,
.tmp = tmp,
});
const files = try db.list(arena);
defer arena.free(files);
const table: tabula.Table(Db.EnvFile, .initOne(.path)) = .{ .items = files };
try out.print("{f}", .{table});
try out.flush();
try db.close(io, arena); // TODO: Defer this
for (files) |*file| {
file.deinit(arena);
}
}
test {
std.testing.refAllDecls(@import("Config.zig"));
std.testing.refAllDecls(@import("Db.zig"));
}
test "enum type" {
const got: root.Type = @enumFromInt(3);
try std.testing.expectEqual(.version, got);
}
test "parse deps" {
const args = &[_][]const u8{"deps"};
const cmd = root.parse(args);
try std.testing.expectEqual(.deps, cmd);
}
test "parse list" {
const args = &[_][]const u8{"list"};
const cmd = root.parse(args);
try std.testing.expectEqual(.list, cmd);
}
test "parse version" {
const args = &[_][]const u8{"version"};
const cmd = root.parse(args);
try std.testing.expectEqual(.version, cmd);
}
test "parse unknown" {
const args = &[_][]const u8{ "bad", "value" };
const cmd = root.parse(args);
try std.testing.expectEqual(.unknown, cmd);
}
test "list returns a table" {
const io = std.testing.io;
const gpa = std.testing.allocator;
var tmp_dir = std.testing.tmpDir(.{});
defer tmp_dir.cleanup();
try tmp_dir.dir.createDir(io, "home", .default_dir);
try tmp_dir.dir.createDir(io, "home/.envr", .default_dir);
try tmp_dir.dir.createDir(io, "home/.ssh", .default_dir);
try tmp_dir.dir.createDir(io, "tmp", .default_dir);
const tmp_dir_path = try tmp_dir.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(tmp_dir_path);
const home = try std.fs.path.join(gpa, &.{ tmp_dir_path, "home" });
defer gpa.free(home);
const tmp = try std.fs.path.join(gpa, &.{ tmp_dir_path, "tmp" });
defer gpa.free(tmp);
try std.Io.Dir.cwd().copyFile(
"fixtures/encrypted-single-file.db.age",
tmp_dir.dir,
"home/.envr/data.age",
io,
.{},
);
try std.Io.Dir.cwd().copyFile(
"fixtures/default_config.json",
tmp_dir.dir,
"home/.envr/config.json",
io,
.{},
);
try std.Io.Dir.cwd().copyFile(
"fixtures/insecure-test-key",
tmp_dir.dir,
"home/.ssh/id_ed25519",
io,
.{},
);
try std.Io.Dir.cwd().copyFile(
"fixtures/insecure-test-key.pub",
tmp_dir.dir,
"home/.ssh/id_ed25519.pub",
io,
.{},
);
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
// Run Test
try list(
io,
std.testing.allocator,
&out.writer,
home,
tmp,
);
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌────────────────────────┐
\\│ path │
\\├────────────────────────┤
\\│ ~/project/.env.example │
\\└────────────────────────┘
\\
, got);
}

311
src/tabula.zig Normal file
View File

@@ -0,0 +1,311 @@
const std = @import("std");
const hor = "";
const tl = "";
const tm = "";
const tr = "";
const sep = "";
const ml = "";
const mm = "";
const mr = "";
const bl = "";
const bm = "";
const br = "";
/// Prepare a TUI table to be written to a writer.
pub fn Table(
comptime T: type,
comptime fields: std.EnumSet(std.meta.FieldEnum(T)),
) type {
return struct {
items: []const T,
pub fn format(self: @This(), writer: *std.Io.Writer) !void {
const max_column_widths = determine_col_widths(T, self.items);
try header(T, fields, &max_column_widths, writer);
// Print body
for (self.items) |item| {
try writer.writeAll(sep);
comptime var itr = fields.iterator();
comptime var i: usize = 0;
inline while (comptime itr.next()) |c| : (i += 1) {
try writer.writeByte(' ');
try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
try writer.print(" {s}", .{sep});
}
try writer.writeAll("\n");
}
// Print post-body
{
try writer.writeAll(bl);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
try writer.writeAll(bm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
try writer.writeAll(hor);
}
}
try writer.writeAll(br ++ "\n");
}
}
};
}
fn determine_col_widths(
T: type,
items: []const T,
) [@typeInfo(T).@"struct".fields.len]usize {
const all_fields = @typeInfo(T).@"struct".fields;
var max_column_widths: [all_fields.len]usize = @splat(0);
for (items) |item| {
inline for (all_fields, 0..) |field, i| {
// TODO: Get str len of item
const value_len = @field(item, field.name).len;
max_column_widths[i] = @max(
max_column_widths[i],
field.name.len,
value_len,
);
}
}
return max_column_widths;
}
// Print the header of a table
fn header(
T: type,
comptime fields: std.EnumSet(std.meta.FieldEnum(T)),
max_column_widths: []const usize,
writer: *std.Io.Writer,
) !void {
// Print Pre-Header
{
try writer.writeAll(tl);
inline for (0..comptime fields.count()) |i| {
if (i > 0) {
try writer.writeAll(tm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
try writer.writeAll(hor);
}
}
try writer.writeAll(tr ++ "\n");
}
// Main Header
{
try writer.writeAll(sep);
comptime var itr = fields.iterator();
comptime var i: usize = 0;
inline while (comptime itr.next()) |field| : (i += 1) {
try writer.writeByte(' ');
try write_aligned(
writer,
@tagName(field),
max_column_widths[i],
.center,
);
try writer.print(" {s}", .{sep});
}
try writer.writeByte('\n');
}
// Print post-header
{
try writer.writeAll(ml);
inline for (0..comptime fields.count()) |i| {
if (i > 0) {
try writer.writeAll(mm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
try writer.writeAll(hor);
}
}
try writer.writeAll(mr ++ "\n");
}
}
fn write_aligned(
writer: *std.Io.Writer,
data: []const u8,
max_width: usize,
alignment: Alignment,
) !void {
std.debug.assert(data.len > 0);
std.debug.assert(max_width >= data.len);
const padding: [2]usize = switch (alignment) {
.left => .{ 0, max_width - data.len },
.right => .{ max_width - data.len, 0 },
.center => blk: {
// Faster to inline the divFloor?
const half = @divFloor(max_width - data.len, 2);
break :blk .{ half, max_width - data.len - half };
},
};
for (0..padding[0]) |_| {
try writer.writeByte(' ');
}
try writer.writeAll(data);
for (0..padding[1]) |_| {
try writer.writeByte(' ');
}
}
const Alignment = enum { left, center, right };
test "can print a simple table" {
const gpa = std.testing.allocator;
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const F = struct { foo: []const u8, bar: []const u8 };
const table: Table(F, .full) = .{
.items = &.{.{ .foo = "bat", .bar = "baz" }},
};
try out.writer.print("{f}", .{table});
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬─────┐
\\│ foo │ bar │
\\├─────┼─────┤
\\│ bat │ baz │
\\└─────┴─────┘
\\
, got);
}
test "can print a table with varying header widths" {
const gpa = std.testing.allocator;
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const F = struct { foo: []const u8, abart: []const u8 };
const table: Table(F, .full) = .{
.items = &.{.{ .foo = "bat", .abart = "baz" }},
};
try out.writer.print("{f}", .{table});
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬───────┐
\\│ foo │ abart │
\\├─────┼───────┤
\\│ bat │ baz │
\\└─────┴───────┘
\\
, got);
}
test "can print a table with varying column widths" {
const gpa = std.testing.allocator;
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const F = struct { foo: []const u8, bar: []const u8 };
const table: Table(F, .full) = .{ .items = &.{.{ .foo = "bat", .bar = "bazzar" }} };
try out.writer.print("{f}", .{table});
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬────────┐
\\│ foo │ bar │
\\├─────┼────────┤
\\│ bat │ bazzar │
\\└─────┴────────┘
\\
, got);
}
test "can print a multi row table with varying column widths" {
const gpa = std.testing.allocator;
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const F = struct { foo: []const u8, bar: []const u8 };
const table: Table(F, .full) = .{
.items = &.{
.{ .foo = "baz", .bar = "quz" },
.{ .foo = "bat", .bar = "bazzar" },
},
};
try out.writer.print("{f}", .{table});
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬────────┐
\\│ foo │ bar │
\\├─────┼────────┤
\\│ baz │ quz │
\\│ bat │ bazzar │
\\└─────┴────────┘
\\
, got);
}
test "can print a table with limited columns" {
const gpa = std.testing.allocator;
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const F = struct { foo: []const u8, bar: []const u8 };
const table: Table(F, .initOne(.foo)) = .{
.items = &.{.{ .foo = "bat", .bar = "baz" }},
};
try out.writer.print("{f}", .{table});
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┐
\\│ foo │
\\├─────┤
\\│ bat │
\\└─────┘
\\
, got);
}

1
zig-vendor/age-ffi/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

1936
zig-vendor/age-ffi/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
[package]
name = "age-ffi"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
[dependencies]
age = { version = "0.11", features = ["armor", "ssh", "plugin", "cli-common"] }
secrecy = "0.10"
libc = "0.2"
[profile.release]
lto = true

View File

@@ -0,0 +1,165 @@
# age-ffi
A Rust FFI wrapper for the [age](https://github.com/str4d/rage) encryption library, with Zig bindings.
## Overview
This library provides C-compatible FFI bindings for the age encryption library, making it easy to use age encryption from other languages. It includes comprehensive Zig bindings and examples.
## Features
- **X25519 encryption** - Standard age public key encryption (`age1...`)
- **SSH key support** - Encrypt to SSH keys (`ssh-ed25519`, `ssh-rsa`)
- **Plugin support** - Full support for age plugins including:
- [age-plugin-se](https://github.com/remko/age-plugin-se) (Secure Enclave on macOS)
- [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey)
- Any other age-compatible plugin
- **Passphrase encryption** - Scrypt-based passphrase encryption
- **Multiple recipients** - Encrypt to multiple recipients at once
- **Armor format** - ASCII-armored output support
- **File operations** - Direct file encryption/decryption
- **Memory-safe API** - Proper error handling and memory management
- **Comprehensive test suite**
## Supported Identity/Recipient Types
| Type | Recipient Format | Identity Format |
|------|-----------------|-----------------|
| X25519 | `age1...` | `AGE-SECRET-KEY-1...` |
| SSH | `ssh-ed25519 ...`, `ssh-rsa ...` | SSH private key file |
| Plugin | `age1<plugin>1...` | `AGE-PLUGIN-<NAME>-1...` |
| Passphrase | N/A | Passphrase string |
## Building
### Rust Library
```bash
cargo build --release
```
This produces `target/release/libage_ffi.a` (static library).
### Zig Bindings
```bash
cd zig
zig build
```
Run the example:
```bash
cd zig
zig build run
```
Run tests:
```bash
cd zig
zig build test
```
## Usage
### Zig
```zig
const age = @import("age");
// Generate a keypair
var keypair = try age.generateKeypair();
defer keypair.deinit();
// Encrypt data
const plaintext = "Hello, World!";
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
defer encrypted.deinit();
// Decrypt data
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
defer decrypted.deinit();
// File operations with plugin support
try age.encryptToFile(plaintext, "age1se1...", "/path/to/output.age");
var content = try age.decryptFile("/path/to/file.age", "/path/to/identities");
defer content.deinit();
```
### C
```c
#include <age_ffi.h>
// Generate keypair
AgeKeypair keypair;
age_generate_keypair(&keypair);
// Encrypt
AgeBuffer encrypted;
age_encrypt(plaintext, plaintext_len, keypair.public_key, &encrypted);
// Decrypt
AgeBuffer decrypted;
age_decrypt(encrypted.data, encrypted.len, keypair.private_key, &decrypted);
// Free resources
age_free_buffer(&encrypted);
age_free_buffer(&decrypted);
age_free_keypair(&keypair);
```
## Plugin Support
This library supports the [age plugin protocol](https://github.com/C2SP/C2SP/blob/main/age.md), allowing encryption and decryption with hardware-backed keys and other plugin-based identities.
### Requirements
- The plugin binary must be in your `$PATH` (e.g., `age-plugin-se`)
- For Secure Enclave: macOS with Touch ID or Apple Watch
### Example with Secure Enclave
```bash
# Install the plugin
brew install age-plugin-se
# Generate a Secure Enclave identity
age-plugin-se --generate -o ~/.age/se-identity.txt
# The library will automatically use the plugin when it sees:
# - Recipients starting with age1se1...
# - Identities starting with AGE-PLUGIN-SE-...
```
## API Reference
### Key Generation
- `age_generate_keypair()` - Generate X25519 keypair
- `age_generate_x25519()` - Generate X25519 keypair (alias)
- `age_x25519_to_public()` - Derive public key from private key
### Encryption
- `age_encrypt()` - Encrypt to a single recipient
- `age_encrypt_multi()` - Encrypt to multiple recipients
- `age_encrypt_armor()` - Encrypt with ASCII armor
- `age_encrypt_passphrase()` - Encrypt with passphrase
- `age_encrypt_to_file()` - Encrypt directly to file
### Decryption
- `age_decrypt()` - Decrypt with identity string
- `age_decrypt_multi()` - Decrypt with multiple identities
- `age_decrypt_file()` - Decrypt file using identity file (supports plugins)
- `age_decrypt_passphrase()` - Decrypt with passphrase
### Utilities
- `age_armor()` - Wrap binary data in ASCII armor
- `age_dearmor()` - Unwrap ASCII-armored data
- `age_validate_recipient()` - Check if recipient string is valid
- `age_validate_identity()` - Check if identity string is valid
- `age_version()` - Get library version
## License
This project is dual-licensed under MIT and Apache-2.0, matching the age library.

View File

@@ -0,0 +1,95 @@
//! ASCII armor utilities.
use crate::helpers::cstr_to_str;
use crate::helpers::string_to_cstr;
use crate::types::{AgeBuffer, AgeResult};
use std::io::{Read, Write};
use std::os::raw::c_char;
/// Wrap binary data in ASCII armor.
///
/// # Arguments
/// * `data` - Pointer to the binary data
/// * `data_len` - Length of the data
/// * `output` - Pointer to receive the armored string
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_armor(
data: *const u8,
data_len: usize,
output: *mut *mut c_char,
) -> AgeResult {
if data.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let data = unsafe { std::slice::from_raw_parts(data, data_len) };
let mut armored = Vec::new();
let mut writer = match age::armor::ArmoredWriter::wrap_output(&mut armored, age::armor::Format::AsciiArmor) {
Ok(w) => w,
Err(_) => return AgeResult::ArmorError,
};
if writer.write_all(data).is_err() {
return AgeResult::ArmorError;
}
if writer.finish().is_err() {
return AgeResult::ArmorError;
}
let armored_str = match String::from_utf8(armored) {
Ok(s) => s,
Err(_) => return AgeResult::ArmorError,
};
let c_output = match string_to_cstr(armored_str) {
Ok(s) => s,
Err(e) => return e,
};
unsafe {
*output = c_output;
}
AgeResult::Success
}
/// Remove ASCII armor from data.
///
/// # Arguments
/// * `armored` - The armored string
/// * `output` - Pointer to receive the binary buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_dearmor(
armored: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let armored_str = match unsafe { cstr_to_str(armored) } {
Ok(s) => s,
Err(e) => return e,
};
let mut reader = age::armor::ArmoredReader::new(armored_str.as_bytes());
let mut dearmored = Vec::new();
if reader.read_to_end(&mut dearmored).is_err() {
return AgeResult::ArmorError;
}
unsafe {
*output = AgeBuffer::from_vec(dearmored);
}
AgeResult::Success
}

View File

@@ -0,0 +1,175 @@
//! Tests for ASCII armor utilities.
use crate::armor::*;
use crate::encrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[test]
fn test_armor_basic() {
let data = b"Hello, this is binary data to armor!";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(data.as_ptr(), data.len(), &mut armored);
assert_eq!(result, AgeResult::Success);
assert!(!armored.is_null());
let armored_str = unsafe { CStr::from_ptr(armored).to_str().unwrap() };
assert!(armored_str.starts_with("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(armored_str.contains("-----END AGE ENCRYPTED FILE-----"));
age_free_string(armored);
}
#[test]
fn test_dearmor_basic() {
let data = b"Test data for dearmoring";
let mut armored: *mut c_char = std::ptr::null_mut();
age_armor(data.as_ptr(), data.len(), &mut armored);
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
let dearmored_slice = unsafe { std::slice::from_raw_parts(dearmored.data, dearmored.len) };
assert_eq!(dearmored_slice, data);
age_free_string(armored);
age_free_buffer(&mut dearmored);
}
#[test]
fn test_armor_round_trip() {
// Test with various data sizes (skip empty - armor requires data)
let test_data = [
b"A".to_vec(),
b"Short".to_vec(),
(0u16..256).map(|i| i as u8).collect::<Vec<u8>>(),
vec![0u8; 1000],
(0..10000).map(|i| (i % 256) as u8).collect::<Vec<u8>>(),
];
for data in &test_data {
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(data.as_ptr(), data.len(), &mut armored);
assert_eq!(result, AgeResult::Success, "Failed to armor data of len {}", data.len());
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success, "Failed to dearmor data of len {}", data.len());
let dearmored_slice = unsafe { std::slice::from_raw_parts(dearmored.data, dearmored.len) };
assert_eq!(dearmored_slice, data.as_slice());
age_free_string(armored);
age_free_buffer(&mut dearmored);
}
}
#[test]
fn test_armor_null_input() {
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(std::ptr::null(), 0, &mut armored);
assert_eq!(result, AgeResult::InvalidInput);
let result = age_armor(b"test".as_ptr(), 4, std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_dearmor_null_input() {
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(std::ptr::null(), &mut dearmored);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_dearmor_null_output() {
let data = b"test";
let mut armored: *mut c_char = std::ptr::null_mut();
age_armor(data.as_ptr(), data.len(), &mut armored);
let result = age_dearmor(armored, std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
age_free_string(armored);
}
#[test]
fn test_dearmor_invalid_armor() {
let invalid_armor = CString::new("This is not valid armor").unwrap();
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(invalid_armor.as_ptr(), &mut dearmored);
// Should still succeed but return the data as-is or fail gracefully
// The ArmoredReader is forgiving and may just return the raw data
// Let's check that it doesn't crash at least
if result == AgeResult::Success {
age_free_buffer(&mut dearmored);
}
}
#[test]
fn test_encrypt_armor_and_dearmor() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test encrypt -> armor -> dearmor -> decrypt";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
assert_eq!(result, AgeResult::Success);
// Dearmor
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
// Decrypt
let mut decrypted = AgeBuffer::null();
let result = crate::decrypt::age_decrypt(
dearmored.data,
dearmored.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_string(armored);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_armor_binary_data() {
// Test with binary data including null bytes
let binary_data: Vec<u8> = (0u16..256).map(|i| i as u8).collect();
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_armor(binary_data.as_ptr(), binary_data.len(), &mut armored);
assert_eq!(result, AgeResult::Success);
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
let dearmored_slice = unsafe { std::slice::from_raw_parts(dearmored.data, dearmored.len) };
assert_eq!(dearmored_slice, binary_data.as_slice());
age_free_string(armored);
age_free_buffer(&mut dearmored);
}

View File

@@ -0,0 +1,299 @@
//! In-memory decryption functions.
use crate::helpers::cstr_to_str;
use crate::types::{AgeBuffer, AgeResult};
use age::ssh;
use std::io::{BufReader, Read};
use std::os::raw::c_char;
use std::str::FromStr;
/// Decrypt data in memory using a single x25519 identity.
/// This is a simple API for common use cases.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `identity` - The private key string (AGE-SECRET-KEY-1...)
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt(
ciphertext: *const u8,
ciphertext_len: usize,
identity: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let identity_str = match unsafe { cstr_to_str(identity) } {
Ok(s) => s,
Err(e) => return e,
};
let identity = match age::x25519::Identity::from_str(identity_str) {
Ok(i) => i,
Err(_) => return AgeResult::InvalidIdentity,
};
let decrypted = match age::decrypt(&identity, ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data in memory using multiple identities.
/// The library will try each identity until one succeeds.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `identities` - Array of identity C strings
/// * `identity_count` - Number of identities
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_multi(
ciphertext: *const u8,
ciphertext_len: usize,
identities: *const *const c_char,
identity_count: usize,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || identities.is_null() || output.is_null() || identity_count == 0 {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let identity_ptrs = unsafe { std::slice::from_raw_parts(identities, identity_count) };
let mut parsed_identities: Vec<Box<dyn age::Identity>> = Vec::new();
for &ptr in identity_ptrs {
let identity_str = match unsafe { cstr_to_str(ptr) } {
Ok(s) => s.trim(),
Err(e) => return e,
};
// Try x25519 first
if let Ok(i) = age::x25519::Identity::from_str(identity_str) {
parsed_identities.push(Box::new(i));
continue;
}
// Skip comments and empty lines
if identity_str.is_empty() || identity_str.starts_with('#') {
continue;
}
return AgeResult::InvalidIdentity;
}
if parsed_identities.is_empty() {
return AgeResult::NoIdentities;
}
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(parsed_identities.iter().map(|i| i.as_ref())) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data using an SSH private key.
/// Supports both Ed25519 and RSA SSH keys.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `ssh_key` - The SSH private key in PEM or OpenSSH format
/// * `passphrase` - Optional passphrase for encrypted SSH keys (can be null)
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_ssh(
ciphertext: *const u8,
ciphertext_len: usize,
ssh_key: *const c_char,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let ssh_key_str = match unsafe { cstr_to_str(ssh_key) } {
Ok(s) => s,
Err(e) => return e,
};
// Parse SSH identity from buffer
let buf_reader = BufReader::new(ssh_key_str.as_bytes());
let identity = match ssh::Identity::from_buffer(buf_reader, None) {
Ok(id) => id,
Err(_) => return AgeResult::SshKeyError,
};
// Handle encrypted SSH keys - keep as ssh::Identity since it implements age::Identity
let identity: ssh::Identity = match identity {
ssh::Identity::Unencrypted(_) => identity,
ssh::Identity::Encrypted(enc) => {
let passphrase_str = if passphrase.is_null() {
return AgeResult::PassphraseRequired;
} else {
match unsafe { cstr_to_str(passphrase) } {
Ok(s) if !s.is_empty() => s,
_ => return AgeResult::PassphraseRequired,
}
};
match enc.decrypt(age::secrecy::SecretString::from(passphrase_str.to_string())) {
Ok(id) => ssh::Identity::Unencrypted(id),
Err(_) => return AgeResult::InvalidPassphrase,
}
}
ssh::Identity::Unsupported(_) => return AgeResult::UnsupportedKey,
};
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data using an SSH private key file.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `ssh_key_path` - Path to the SSH private key file
/// * `passphrase` - Optional passphrase for encrypted SSH keys (can be null)
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_ssh_file(
ciphertext: *const u8,
ciphertext_len: usize,
ssh_key_path: *const c_char,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let path_str = match unsafe { cstr_to_str(ssh_key_path) } {
Ok(s) => s,
Err(e) => return e,
};
// The filename is passed as a hint for error messages
let filename = Some(path_str.to_string());
// Read and parse SSH key file
let ssh_key_data = match std::fs::read(path_str) {
Ok(data) => data,
Err(_) => return AgeResult::IoError,
};
let buf_reader = BufReader::new(ssh_key_data.as_slice());
let identity = match ssh::Identity::from_buffer(buf_reader, filename) {
Ok(id) => id,
Err(_) => return AgeResult::SshKeyError,
};
// Handle encrypted SSH keys - keep as ssh::Identity since it implements age::Identity
let identity: ssh::Identity = match identity {
ssh::Identity::Unencrypted(_) => identity,
ssh::Identity::Encrypted(enc) => {
// Parse passphrase if provided
let passphrase_str = if passphrase.is_null() {
return AgeResult::PassphraseRequired;
} else {
match unsafe { cstr_to_str(passphrase) } {
Ok(s) if !s.is_empty() => s,
_ => return AgeResult::PassphraseRequired,
}
};
match enc.decrypt(age::secrecy::SecretString::from(passphrase_str.to_string())) {
Ok(id) => ssh::Identity::Unencrypted(id),
Err(_) => return AgeResult::InvalidPassphrase,
}
}
ssh::Identity::Unsupported(_) => return AgeResult::UnsupportedKey,
};
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}

View File

@@ -0,0 +1,430 @@
//! Tests for in-memory decryption functions.
use crate::decrypt::*;
use crate::encrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CString;
use std::os::raw::c_char;
#[test]
fn test_basic_decrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Decryption test message";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_null_ciphertext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
std::ptr::null(),
0,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_null_output() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_invalid_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let invalid_identity = CString::new("not-a-valid-identity").unwrap();
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
invalid_identity.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidIdentity);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_wrong_key() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Secret message";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair1.public_key, &mut encrypted);
// Try to decrypt with wrong key
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair2.private_key, // Wrong key!
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_decrypt_corrupted_ciphertext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Original message";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Corrupt the ciphertext
if encrypted.len > 50 {
unsafe {
*encrypted.data.add(50) ^= 0xFF;
}
}
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
// Should fail (either DecryptionFailed or other error depending on what was corrupted)
assert_ne!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_with_multiple_identities() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Multi-identity message";
let recipients: [*const c_char; 1] = [keypair1.public_key as *const c_char];
let mut encrypted = AgeBuffer::null();
age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
1,
false,
&mut encrypted,
);
// Decrypt with multiple identities (one valid, one invalid for this message)
let identities: [*const c_char; 2] = [
keypair2.private_key as *const c_char, // Wrong key first
keypair1.private_key as *const c_char, // Correct key
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
2,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_decrypt_multi_empty_identities() {
let plaintext = b"test";
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
std::ptr::null(),
0,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_null_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
std::ptr::null(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_null_identity_in_array() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Array with a null pointer inside
let identities: [*const c_char; 2] = [
keypair.private_key as *const c_char,
std::ptr::null(),
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
2,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_with_comments_and_empty() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test with comments";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Mix of comments, empty strings, and valid identity
let comment = CString::new("# This is a comment").unwrap();
let empty = CString::new("").unwrap();
let identities: [*const c_char; 3] = [
comment.as_ptr(),
empty.as_ptr(),
keypair.private_key as *const c_char,
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
3,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_only_comments() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Only comments and empty - no valid identities
let comment1 = CString::new("# Comment 1").unwrap();
let comment2 = CString::new("# Comment 2").unwrap();
let empty = CString::new("").unwrap();
let identities: [*const c_char; 3] = [
comment1.as_ptr(),
comment2.as_ptr(),
empty.as_ptr(),
];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
3,
&mut decrypted,
);
assert_eq!(result, AgeResult::NoIdentities);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_invalid_identity_format() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair.public_key, &mut encrypted);
// Invalid identity (not a comment, not empty, not valid key)
let invalid = CString::new("invalid-key-format").unwrap();
let identities: [*const c_char; 1] = [invalid.as_ptr()];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
1,
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidIdentity);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_corrupted_ciphertext() {
let corrupted = b"not valid age encrypted data at all";
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let identities: [*const c_char; 1] = [keypair.private_key as *const c_char];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
corrupted.as_ptr(),
corrupted.len(),
identities.as_ptr(),
1,
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_multi_wrong_key_only() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
age_encrypt(plaintext.as_ptr(), plaintext.len(), keypair1.public_key, &mut encrypted);
// Only provide wrong key
let identities: [*const c_char; 1] = [keypair2.private_key as *const c_char];
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_multi(
encrypted.data,
encrypted.len,
identities.as_ptr(),
1,
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}

View File

@@ -0,0 +1,210 @@
//! In-memory encryption functions.
use crate::helpers::{cstr_to_str, string_to_cstr};
use crate::types::{AgeBuffer, AgeResult};
use std::io::Write;
use std::os::raw::c_char;
/// Encrypt data in memory using a single x25519 recipient.
/// This is a simple API for common use cases.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `recipient` - The recipient public key (age1...)
/// * `output` - Pointer to receive the encrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt(
plaintext: *const u8,
plaintext_len: usize,
recipient: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if plaintext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient = match recipient_str.parse::<age::x25519::Recipient>() {
Ok(r) => r,
Err(_) => return AgeResult::InvalidRecipient,
};
let encrypted = match age::encrypt(&recipient, plaintext) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
unsafe {
*output = AgeBuffer::from_vec(encrypted);
}
AgeResult::Success
}
/// Encrypt data in memory using multiple recipients.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `recipients` - Array of recipient public key C strings
/// * `recipient_count` - Number of recipients
/// * `armor` - If true, output will be ASCII-armored
/// * `output` - Pointer to receive the encrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_multi(
plaintext: *const u8,
plaintext_len: usize,
recipients: *const *const c_char,
recipient_count: usize,
armor: bool,
output: *mut AgeBuffer,
) -> AgeResult {
if plaintext.is_null() || recipients.is_null() || output.is_null() || recipient_count == 0 {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let recipient_ptrs = unsafe { std::slice::from_raw_parts(recipients, recipient_count) };
let mut parsed_recipients: Vec<Box<dyn age::Recipient + Send>> = Vec::new();
for &ptr in recipient_ptrs {
let recipient_str = match unsafe { cstr_to_str(ptr) } {
Ok(s) => s.trim(),
Err(e) => return e,
};
// Try x25519 first
if let Ok(r) = recipient_str.parse::<age::x25519::Recipient>() {
parsed_recipients.push(Box::new(r));
continue;
}
// Try SSH
if let Ok(r) = recipient_str.parse::<age::ssh::Recipient>() {
parsed_recipients.push(Box::new(r));
continue;
}
return AgeResult::InvalidRecipient;
}
if parsed_recipients.is_empty() {
return AgeResult::NoRecipients;
}
let encryptor = match age::Encryptor::with_recipients(
parsed_recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient)
) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
let mut encrypted = Vec::new();
let result = if armor {
let armor_writer = age::armor::ArmoredWriter::wrap_output(&mut encrypted, age::armor::Format::AsciiArmor)
.map_err(|_| AgeResult::ArmorError);
match armor_writer {
Ok(armor) => {
match encryptor.wrap_output(armor) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
match writer.finish() {
Ok(armor) => armor.finish().map_err(|_| AgeResult::ArmorError),
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(e) => return e,
}
} else {
match encryptor.wrap_output(&mut encrypted) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
writer.finish().map_err(|_| AgeResult::EncryptionFailed)
}
Err(_) => return AgeResult::EncryptionFailed,
}
};
if result.is_err() {
return AgeResult::EncryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(encrypted);
}
AgeResult::Success
}
/// Encrypt data with ASCII armor for text-safe output.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `recipient` - The recipient public key (age1...)
/// * `output` - Pointer to receive the armored string (null-terminated)
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_armor(
plaintext: *const u8,
plaintext_len: usize,
recipient: *const c_char,
output: *mut *mut c_char,
) -> AgeResult {
if plaintext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient = match recipient_str.parse::<age::x25519::Recipient>() {
Ok(r) => r,
Err(_) => return AgeResult::InvalidRecipient,
};
let encrypted = match age::encrypt_and_armor(&recipient, plaintext) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
let c_output = match string_to_cstr(encrypted) {
Ok(s) => s,
Err(e) => return e,
};
unsafe {
*output = c_output;
}
AgeResult::Success
}

View File

@@ -0,0 +1,232 @@
//! Tests for in-memory encryption functions.
use crate::encrypt::*;
use crate::decrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CString;
use std::os::raw::c_char;
#[test]
fn test_basic_encrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Hello, encryption!";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
assert!(!encrypted.data.is_null());
assert!(encrypted.len > plaintext.len()); // Encrypted should be larger
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_null_plaintext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
std::ptr::null(),
0,
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_null_output() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_invalid_recipient() {
let invalid_recipient = CString::new("not-a-valid-recipient").unwrap();
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
invalid_recipient.as_ptr(),
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidRecipient);
}
#[test]
fn test_encrypt_multi_two_recipients() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Message for both recipients";
let recipients: [*const c_char; 2] = [
keypair1.public_key as *const c_char,
keypair2.public_key as *const c_char,
];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
2,
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Both recipients should be able to decrypt
let mut decrypted1 = AgeBuffer::null();
let result = age_decrypt(encrypted.data, encrypted.len, keypair1.private_key, &mut decrypted1);
assert_eq!(result, AgeResult::Success);
let mut decrypted2 = AgeBuffer::null();
let result = age_decrypt(encrypted.data, encrypted.len, keypair2.private_key, &mut decrypted2);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted1);
age_free_buffer(&mut decrypted2);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_encrypt_multi_with_armor() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored multi-recipient message";
let recipients: [*const c_char; 1] = [keypair.public_key as *const c_char];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
1,
true, // armor
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Check it's armored
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
let encrypted_str = std::str::from_utf8(encrypted_slice).unwrap();
assert!(encrypted_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_multi_empty_recipients() {
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
std::ptr::null(),
0,
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_armor() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored message";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
assert_eq!(result, AgeResult::Success);
assert!(!armored.is_null());
let armored_str = unsafe { std::ffi::CStr::from_ptr(armored).to_str().unwrap() };
assert!(armored_str.starts_with("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(armored_str.contains("-----END AGE ENCRYPTED FILE-----"));
age_free_string(armored);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_various_sizes() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let sizes = [0, 1, 16, 256, 1024, 4096, 65536];
for size in sizes {
let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success, "Failed for size {}", size);
// Verify we can decrypt it back
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(encrypted.data, encrypted.len, keypair.private_key, &mut decrypted);
assert_eq!(result, AgeResult::Success, "Decrypt failed for size {}", size);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext.as_slice(), "Mismatch for size {}", size);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
age_free_keypair(&mut keypair);
}

View File

@@ -0,0 +1,351 @@
//! File-based encryption and decryption operations.
use crate::helpers::{cstr_to_str, cstr_to_string};
use crate::types::{AgeBuffer, AgeResult};
use age::secrecy::SecretString;
use std::fs::File;
use std::io::{Read, Write};
use std::os::raw::c_char;
use std::str::FromStr;
/// Encrypt data to a file using a recipient.
///
/// # Arguments
/// * `plaintext` - The data to encrypt
/// * `plaintext_len` - Length of the plaintext
/// * `output_path` - Path to write the encrypted .age file
/// * `recipient` - The recipient public key (age1...) or path to recipients file
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_to_file(
plaintext: *const c_char,
plaintext_len: usize,
output_path: *const c_char,
recipient: *const c_char,
) -> AgeResult {
if plaintext.is_null() || output_path.is_null() || recipient.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext as *const u8, plaintext_len) };
let output_path = match unsafe { cstr_to_str(output_path) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
// Parse recipients - could be a file path or a direct recipient key
// Supports: x25519 (age1...), plugin (age1<plugin>1...), and ssh (ssh-...)
let mut recipients: Vec<Box<dyn age::Recipient + Send>> = Vec::new();
let mut plugin_recipients: Vec<age::plugin::Recipient> = Vec::new();
let recipient_lines: Vec<&str> = if recipient_str.starts_with("age1") || recipient_str.starts_with("ssh-") {
vec![recipient_str]
} else {
// Assume it's a file path containing recipients
match std::fs::read_to_string(recipient_str) {
Ok(contents) => {
// We need to own the string for the lines
let contents_leaked: &'static str = Box::leak(contents.into_boxed_str());
contents_leaked
.lines()
.filter(|line| !line.starts_with('#') && !line.is_empty())
.map(|line| line.trim())
.collect()
}
Err(_) => return AgeResult::IoError,
}
};
for line in recipient_lines {
// Try x25519 first
if let Ok(r) = line.parse::<age::x25519::Recipient>() {
recipients.push(Box::new(r));
continue;
}
// Then try plugin recipient - collect these separately
if let Ok(r) = line.parse::<age::plugin::Recipient>() {
plugin_recipients.push(r);
continue;
}
// Finally try SSH
if let Ok(r) = line.parse::<age::ssh::Recipient>() {
recipients.push(Box::new(r));
continue;
}
// Skip unrecognized lines
}
// Create plugin recipients wrapper if we have any plugin recipients
// Group them by plugin name
if !plugin_recipients.is_empty() {
use std::collections::HashMap;
let mut by_plugin: HashMap<String, Vec<age::plugin::Recipient>> = HashMap::new();
for r in plugin_recipients {
by_plugin.entry(r.plugin().to_string()).or_default().push(r);
}
for (plugin_name, plugin_recs) in by_plugin {
match age::plugin::RecipientPluginV1::new(
&plugin_name,
&plugin_recs,
&[],
age::NoCallbacks,
) {
Ok(plugin) => recipients.push(Box::new(plugin)),
Err(_) => return AgeResult::InvalidRecipient,
}
}
}
if recipients.is_empty() {
return AgeResult::InvalidRecipient;
}
let output_file = match File::create(output_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let encryptor = match age::Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref() as &dyn age::Recipient)) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
let mut writer = match encryptor.wrap_output(output_file) {
Ok(w) => w,
Err(_) => return AgeResult::EncryptionFailed,
};
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
if writer.finish().is_err() {
return AgeResult::EncryptionFailed;
}
AgeResult::Success
}
/// Encrypt data to a file with ASCII armor.
#[no_mangle]
pub extern "C" fn age_encrypt_to_file_armor(
plaintext: *const u8,
plaintext_len: usize,
output_path: *const c_char,
recipient: *const c_char,
) -> AgeResult {
if plaintext.is_null() || output_path.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let output_path = match unsafe { cstr_to_str(output_path) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(e) => return e,
};
let recipient = match recipient_str.parse::<age::x25519::Recipient>() {
Ok(r) => r,
Err(_) => return AgeResult::InvalidRecipient,
};
let encrypted = match age::encrypt_and_armor(&recipient, plaintext) {
Ok(e) => e,
Err(_) => return AgeResult::EncryptionFailed,
};
if std::fs::write(output_path, encrypted).is_err() {
return AgeResult::IoError;
}
AgeResult::Success
}
/// Decrypt data from a file using an identity file.
///
/// This function supports all identity types including:
/// - Standard x25519 identities (AGE-SECRET-KEY-...)
/// - SSH identities
/// - Plugin identities (AGE-PLUGIN-...)
#[no_mangle]
pub extern "C" fn age_decrypt_file(
encrypted_path: *const c_char,
identity_path: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let encrypted_path = match unsafe { cstr_to_str(encrypted_path) } {
Ok(s) => s,
Err(e) => return e,
};
let identity_path = match unsafe { cstr_to_str(identity_path) } {
Ok(s) => s,
Err(e) => return e,
};
// Use IdentityFile to parse the identity file - this supports all identity types
// including plugin identities (AGE-PLUGIN-...)
let identity_file = match age::IdentityFile::from_file(identity_path.to_string()) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
// Get all identities from the file
let identities = match identity_file.into_identities() {
Ok(ids) => ids,
Err(_) => return AgeResult::InvalidIdentity,
};
if identities.is_empty() {
return AgeResult::InvalidIdentity;
}
let encrypted_file = match File::open(encrypted_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let decryptor = match age::Decryptor::new(encrypted_file) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt data from a file using a single identity string.
#[no_mangle]
pub extern "C" fn age_decrypt_file_with_identity(
encrypted_path: *const c_char,
identity: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let encrypted_path = match unsafe { cstr_to_str(encrypted_path) } {
Ok(s) => s,
Err(e) => return e,
};
let identity_str = match unsafe { cstr_to_str(identity) } {
Ok(s) => s,
Err(e) => return e,
};
let identity = match age::x25519::Identity::from_str(identity_str) {
Ok(i) => i,
Err(_) => return AgeResult::InvalidIdentity,
};
let encrypted_file = match File::open(encrypted_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let decryptor = match age::Decryptor::new(encrypted_file) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}
/// Decrypt a file using a passphrase.
#[no_mangle]
pub extern "C" fn age_decrypt_file_passphrase(
encrypted_path: *const c_char,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if output.is_null() {
return AgeResult::InvalidInput;
}
let encrypted_path = match unsafe { cstr_to_str(encrypted_path) } {
Ok(s) => s,
Err(e) => return e,
};
let passphrase_str = match unsafe { cstr_to_string(passphrase) } {
Ok(s) => s,
Err(e) => return e,
};
let secret = SecretString::from(passphrase_str);
let identity = age::scrypt::Identity::new(secret);
let encrypted_file = match File::open(encrypted_path) {
Ok(f) => f,
Err(_) => return AgeResult::IoError,
};
let decryptor = match age::Decryptor::new(encrypted_file) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}

View File

@@ -0,0 +1,808 @@
//! Tests for file-based encryption and decryption functions.
use crate::file::*;
use crate::keys::*;
use crate::memory::*;
use crate::passphrase::*;
use crate::types::*;
use std::ffi::CString;
use std::fs;
use std::io::Write;
fn create_temp_file(suffix: &str) -> String {
let temp_dir = std::env::temp_dir();
let unique_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{}/age_test_{}_{}", temp_dir.display(), unique_id, suffix)
}
// ============= age_encrypt_to_file tests =============
#[test]
fn test_encrypt_to_file_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Hello, file encryption!";
let output_path = create_temp_file("encrypted.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
assert!(std::path::Path::new(&output_path).exists());
// Clean up
fs::remove_file(&output_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_null_plaintext() {
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let recipient = CString::new("age1test").unwrap();
let result = age_encrypt_to_file(
std::ptr::null(),
0,
output_path_c.as_ptr(),
recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_to_file_null_output_path() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
std::ptr::null(),
keypair.public_key,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_null_recipient() {
let plaintext = b"test";
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
std::ptr::null(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_to_file_invalid_recipient() {
let plaintext = b"test";
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let invalid_recipient = CString::new("age1invalid_not_a_real_key").unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
invalid_recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidRecipient);
}
#[test]
fn test_encrypt_to_file_and_decrypt_with_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Round trip file encryption test!";
let output_path = create_temp_file("roundtrip.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
// Encrypt to file
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
output_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Decrypt with identity string
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
output_path_c.as_ptr(),
keypair.private_key,
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
// Clean up
fs::remove_file(&output_path).ok();
age_free_buffer(&mut output);
age_free_keypair(&mut keypair);
}
// ============= age_encrypt_to_file_armor tests =============
#[test]
fn test_encrypt_to_file_armor_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored file test";
let output_path = create_temp_file("armored.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let result = age_encrypt_to_file_armor(
plaintext.as_ptr(),
plaintext.len(),
output_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Verify the file is armored
let contents = fs::read_to_string(&output_path).unwrap();
assert!(contents.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
assert!(contents.contains("-----END AGE ENCRYPTED FILE-----"));
// Clean up
fs::remove_file(&output_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_armor_null_plaintext() {
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let recipient = CString::new("age1test").unwrap();
let result = age_encrypt_to_file_armor(
std::ptr::null(),
0,
output_path_c.as_ptr(),
recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_encrypt_to_file_armor_null_output_path() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"test";
let result = age_encrypt_to_file_armor(
plaintext.as_ptr(),
plaintext.len(),
std::ptr::null(),
keypair.public_key,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_to_file_armor_invalid_recipient() {
let plaintext = b"test";
let output_path = create_temp_file("test.age");
let output_path_c = CString::new(output_path.as_str()).unwrap();
let invalid_recipient = CString::new("not-a-recipient").unwrap();
let result = age_encrypt_to_file_armor(
plaintext.as_ptr(),
plaintext.len(),
output_path_c.as_ptr(),
invalid_recipient.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidRecipient);
}
// ============= age_decrypt_file tests =============
#[test]
fn test_decrypt_file_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Decrypt from identity file test";
// Create encrypted file
let encrypted_path = create_temp_file("encrypted.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Create identity file
let identity_path = create_temp_file("identity.txt");
let private_key = unsafe { std::ffi::CStr::from_ptr(keypair.private_key).to_str().unwrap() };
fs::write(&identity_path, private_key).unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
// Decrypt
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
// Clean up
fs::remove_file(&encrypted_path).ok();
fs::remove_file(&identity_path).ok();
age_free_buffer(&mut output);
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_null_output() {
let encrypted_path = CString::new("/tmp/test.age").unwrap();
let identity_path = CString::new("/tmp/identity.txt").unwrap();
let result = age_decrypt_file(
encrypted_path.as_ptr(),
identity_path.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_null_encrypted_path() {
let identity_path = CString::new("/tmp/identity.txt").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
std::ptr::null(),
identity_path.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_nonexistent_identity_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create a real encrypted file
let plaintext = b"test";
let encrypted_path = create_temp_file("test_enc.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Try to decrypt with nonexistent identity file
let identity_path = CString::new("/nonexistent/identity.txt").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::IoError);
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_nonexistent_encrypted_file() {
// Create a valid identity file
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let identity_path = create_temp_file("identity.txt");
let private_key = unsafe { std::ffi::CStr::from_ptr(keypair.private_key).to_str().unwrap() };
fs::write(&identity_path, private_key).unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
let encrypted_path = CString::new("/nonexistent/encrypted.age").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::IoError);
fs::remove_file(&identity_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_empty_identity_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create encrypted file
let plaintext = b"test";
let encrypted_path = create_temp_file("enc.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Create empty identity file
let identity_path = create_temp_file("empty_identity.txt");
fs::write(&identity_path, "").unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidIdentity);
fs::remove_file(&encrypted_path).ok();
fs::remove_file(&identity_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_with_comments_in_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create encrypted file
let plaintext = b"test with comments";
let encrypted_path = create_temp_file("enc_comments.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
// Create identity file with comments
let identity_path = create_temp_file("identity_with_comments.txt");
let private_key = unsafe { std::ffi::CStr::from_ptr(keypair.private_key).to_str().unwrap() };
let content = format!("# This is a comment\n\n{}\n# Another comment", private_key);
fs::write(&identity_path, content).unwrap();
let identity_path_c = CString::new(identity_path.as_str()).unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file(
encrypted_path_c.as_ptr(),
identity_path_c.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
fs::remove_file(&encrypted_path).ok();
fs::remove_file(&identity_path).ok();
age_free_buffer(&mut output);
age_free_keypair(&mut keypair);
}
// ============= age_decrypt_file_with_identity tests =============
#[test]
fn test_decrypt_file_with_identity_null_output() {
let encrypted_path = CString::new("/tmp/test.age").unwrap();
let identity = CString::new("AGE-SECRET-KEY-1TEST").unwrap();
let result = age_decrypt_file_with_identity(
encrypted_path.as_ptr(),
identity.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_with_identity_null_path() {
let identity = CString::new("AGE-SECRET-KEY-1TEST").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
std::ptr::null(),
identity.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_with_identity_invalid_identity() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create encrypted file
let plaintext = b"test";
let encrypted_path = create_temp_file("enc_invalid_id.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair.public_key,
);
assert_eq!(result, AgeResult::Success);
let invalid_identity = CString::new("not-a-valid-identity").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
invalid_identity.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidIdentity);
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair);
}
#[test]
fn test_decrypt_file_with_identity_wrong_key() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
// Encrypt with keypair1
let plaintext = b"secret message";
let encrypted_path = create_temp_file("wrong_key.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
keypair1.public_key,
);
assert_eq!(result, AgeResult::Success);
// Try to decrypt with keypair2
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair2.private_key,
&mut output,
);
assert_eq!(result, AgeResult::DecryptionFailed);
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_decrypt_file_with_identity_nonexistent_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let encrypted_path = CString::new("/nonexistent/file.age").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path.as_ptr(),
keypair.private_key,
&mut output,
);
assert_eq!(result, AgeResult::IoError);
age_free_keypair(&mut keypair);
}
// ============= age_decrypt_file_passphrase tests =============
#[test]
fn test_decrypt_file_passphrase_basic() {
let passphrase = CString::new("mysecretpassword").unwrap();
let plaintext = b"Passphrase protected content";
// Encrypt with passphrase first (using in-memory function)
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Write encrypted content to file
let encrypted_path = create_temp_file("passphrase.age");
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
fs::write(&encrypted_path, encrypted_slice).unwrap();
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
// Decrypt file with passphrase
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
encrypted_path_c.as_ptr(),
passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::Success);
let decrypted = unsafe { std::slice::from_raw_parts(output.data, output.len) };
assert_eq!(decrypted, plaintext);
// Clean up
fs::remove_file(&encrypted_path).ok();
age_free_buffer(&mut encrypted);
age_free_buffer(&mut output);
}
#[test]
fn test_decrypt_file_passphrase_null_output() {
let encrypted_path = CString::new("/tmp/test.age").unwrap();
let passphrase = CString::new("password").unwrap();
let result = age_decrypt_file_passphrase(
encrypted_path.as_ptr(),
passphrase.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_passphrase_null_path() {
let passphrase = CString::new("password").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
std::ptr::null(),
passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_decrypt_file_passphrase_wrong_passphrase() {
let passphrase = CString::new("correctpassword").unwrap();
let wrong_passphrase = CString::new("wrongpassword").unwrap();
let plaintext = b"Secret content";
// Encrypt with passphrase
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Write to file
let encrypted_path = create_temp_file("wrong_pass.age");
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
fs::write(&encrypted_path, encrypted_slice).unwrap();
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
// Try to decrypt with wrong passphrase
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
encrypted_path_c.as_ptr(),
wrong_passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::DecryptionFailed);
// Clean up
fs::remove_file(&encrypted_path).ok();
age_free_buffer(&mut encrypted);
}
#[test]
fn test_decrypt_file_passphrase_nonexistent_file() {
let passphrase = CString::new("password").unwrap();
let encrypted_path = CString::new("/nonexistent/passphrase.age").unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_passphrase(
encrypted_path.as_ptr(),
passphrase.as_ptr(),
&mut output,
);
assert_eq!(result, AgeResult::IoError);
}
// ============= Recipient file tests =============
#[test]
fn test_encrypt_to_file_with_recipients_file() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
// Create recipients file
let recipients_path = create_temp_file("recipients.txt");
let pub_key1 = unsafe { std::ffi::CStr::from_ptr(keypair1.public_key).to_str().unwrap() };
let pub_key2 = unsafe { std::ffi::CStr::from_ptr(keypair2.public_key).to_str().unwrap() };
let content = format!("# Comment line\n{}\n{}\n", pub_key1, pub_key2);
fs::write(&recipients_path, content).unwrap();
let recipients_path_c = CString::new(recipients_path.as_str()).unwrap();
// Encrypt to file
let plaintext = b"Multi-recipient from file test";
let encrypted_path = create_temp_file("multi_recip.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
recipients_path_c.as_ptr(),
);
assert_eq!(result, AgeResult::Success);
// Both recipients should be able to decrypt
let mut output1 = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair1.private_key,
&mut output1,
);
assert_eq!(result, AgeResult::Success);
let mut output2 = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair2.private_key,
&mut output2,
);
assert_eq!(result, AgeResult::Success);
// Clean up
fs::remove_file(&recipients_path).ok();
fs::remove_file(&encrypted_path).ok();
age_free_buffer(&mut output1);
age_free_buffer(&mut output2);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_encrypt_to_file_empty_recipients_file() {
let plaintext = b"test";
let encrypted_path = create_temp_file("empty_recip.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
// Create empty recipients file
let recipients_path = create_temp_file("empty_recipients.txt");
fs::write(&recipients_path, "# Only comments\n\n").unwrap();
let recipients_path_c = CString::new(recipients_path.as_str()).unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
recipients_path_c.as_ptr(),
);
assert_eq!(result, AgeResult::InvalidRecipient);
// Clean up
fs::remove_file(&recipients_path).ok();
}
#[test]
fn test_encrypt_to_file_nonexistent_recipients_file() {
let plaintext = b"test";
let encrypted_path = create_temp_file("test.age");
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let recipients_path = CString::new("/nonexistent/recipients.txt").unwrap();
let result = age_encrypt_to_file(
plaintext.as_ptr() as *const i8,
plaintext.len(),
encrypted_path_c.as_ptr(),
recipients_path.as_ptr(),
);
assert_eq!(result, AgeResult::IoError);
}
#[test]
fn test_decrypt_file_corrupted_file() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Create corrupted encrypted file
let encrypted_path = create_temp_file("corrupted.age");
fs::write(&encrypted_path, "not valid age encrypted content").unwrap();
let encrypted_path_c = CString::new(encrypted_path.as_str()).unwrap();
let mut output = AgeBuffer::null();
let result = age_decrypt_file_with_identity(
encrypted_path_c.as_ptr(),
keypair.private_key,
&mut output,
);
assert_eq!(result, AgeResult::DecryptionFailed);
// Clean up
fs::remove_file(&encrypted_path).ok();
age_free_keypair(&mut keypair);
}

View File

@@ -0,0 +1,27 @@
//! Internal helper functions for FFI conversions.
use crate::types::AgeResult;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
/// Safely convert a C string pointer to a Rust &str
pub unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Result<&'a str, AgeResult> {
if ptr.is_null() {
return Err(AgeResult::InvalidInput);
}
CStr::from_ptr(ptr)
.to_str()
.map_err(|_| AgeResult::InvalidUtf8)
}
/// Safely convert a C string pointer to a Rust String
pub unsafe fn cstr_to_string(ptr: *const c_char) -> Result<String, AgeResult> {
cstr_to_str(ptr).map(|s| s.to_owned())
}
/// Convert a Rust String to a C string pointer (caller must free)
pub fn string_to_cstr(s: String) -> Result<*mut c_char, AgeResult> {
CString::new(s)
.map(|cs| cs.into_raw())
.map_err(|_| AgeResult::InvalidInput)
}

View File

@@ -0,0 +1,92 @@
//! Key generation and derivation functions.
use crate::helpers::{cstr_to_str, string_to_cstr};
use crate::types::{AgeKeypair, AgeResult};
use age::secrecy::ExposeSecret;
use std::ffi::CString;
use std::os::raw::c_char;
use std::str::FromStr;
/// Generate a new age x25519 keypair.
///
/// # Arguments
/// * `keypair` - Pointer to receive the generated keypair
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_generate_x25519(keypair: *mut AgeKeypair) -> AgeResult {
if keypair.is_null() {
return AgeResult::InvalidInput;
}
let identity = age::x25519::Identity::generate();
let public_key = identity.to_public().to_string();
let private_key = identity.to_string().expose_secret().to_string();
let c_public = match string_to_cstr(public_key) {
Ok(s) => s,
Err(e) => return e,
};
let c_private = match string_to_cstr(private_key) {
Ok(s) => s,
Err(e) => {
unsafe { drop(CString::from_raw(c_public)); }
return e;
}
};
unsafe {
(*keypair).public_key = c_public;
(*keypair).private_key = c_private;
}
AgeResult::Success
}
/// Alias for age_generate_x25519 for backwards compatibility.
#[no_mangle]
pub extern "C" fn age_generate_keypair(keypair: *mut AgeKeypair) -> AgeResult {
age_generate_x25519(keypair)
}
/// Derive the public key from a private x25519 identity.
///
/// # Arguments
/// * `private_key` - The private key string (AGE-SECRET-KEY-1...)
/// * `public_key` - Pointer to receive the public key string
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_x25519_to_public(
private_key: *const c_char,
public_key: *mut *mut c_char,
) -> AgeResult {
if public_key.is_null() {
return AgeResult::InvalidInput;
}
let private_str = match unsafe { cstr_to_str(private_key) } {
Ok(s) => s,
Err(e) => return e,
};
let identity = match age::x25519::Identity::from_str(private_str) {
Ok(i) => i,
Err(_) => return AgeResult::InvalidIdentity,
};
let public_str = identity.to_public().to_string();
let c_public = match string_to_cstr(public_str) {
Ok(s) => s,
Err(e) => return e,
};
unsafe {
*public_key = c_public;
}
AgeResult::Success
}

View File

@@ -0,0 +1,122 @@
//! Tests for key generation and derivation functions.
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CStr;
#[test]
fn test_generate_x25519_keypair() {
let mut keypair = AgeKeypair::null();
let result = age_generate_x25519(&mut keypair);
assert_eq!(result, AgeResult::Success);
assert!(!keypair.public_key.is_null());
assert!(!keypair.private_key.is_null());
unsafe {
let public = CStr::from_ptr(keypair.public_key).to_str().unwrap();
let private = CStr::from_ptr(keypair.private_key).to_str().unwrap();
assert!(public.starts_with("age1"), "Public key should start with 'age1'");
assert!(private.starts_with("AGE-SECRET-KEY-1"), "Private key should start with 'AGE-SECRET-KEY-1'");
// Check key lengths are reasonable
assert!(public.len() > 50, "Public key should be at least 50 chars");
assert!(private.len() > 50, "Private key should be at least 50 chars");
}
age_free_keypair(&mut keypair);
}
#[test]
fn test_generate_keypair_alias() {
let mut keypair = AgeKeypair::null();
let result = age_generate_keypair(&mut keypair);
assert_eq!(result, AgeResult::Success);
assert!(!keypair.public_key.is_null());
assert!(!keypair.private_key.is_null());
age_free_keypair(&mut keypair);
}
#[test]
fn test_generate_x25519_null_pointer() {
let result = age_generate_x25519(std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_derive_public_key() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut derived_public: *mut std::os::raw::c_char = std::ptr::null_mut();
let result = age_x25519_to_public(keypair.private_key, &mut derived_public);
assert_eq!(result, AgeResult::Success);
assert!(!derived_public.is_null());
// The derived public key should match the original
let original = unsafe { CStr::from_ptr(keypair.public_key).to_str().unwrap() };
let derived = unsafe { CStr::from_ptr(derived_public).to_str().unwrap() };
assert_eq!(original, derived);
age_free_string(derived_public);
age_free_keypair(&mut keypair);
}
#[test]
fn test_derive_public_key_invalid_input() {
use std::ffi::CString;
let mut derived_public: *mut std::os::raw::c_char = std::ptr::null_mut();
// Null output pointer
let result = age_x25519_to_public(std::ptr::null(), std::ptr::null_mut());
assert_eq!(result, AgeResult::InvalidInput);
// Invalid private key
let invalid_key = CString::new("not-a-valid-key").unwrap();
let result = age_x25519_to_public(invalid_key.as_ptr(), &mut derived_public);
assert_eq!(result, AgeResult::InvalidIdentity);
}
#[test]
fn test_derive_public_key_null_private_key() {
let mut derived_public: *mut std::os::raw::c_char = std::ptr::null_mut();
// Null private key but valid output pointer
let result = age_x25519_to_public(std::ptr::null(), &mut derived_public);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_multiple_keypair_generation() {
// Generate multiple keypairs and ensure they're all unique
let mut keypairs: Vec<AgeKeypair> = Vec::new();
for _ in 0..10 {
let mut keypair = AgeKeypair::null();
let result = age_generate_x25519(&mut keypair);
assert_eq!(result, AgeResult::Success);
keypairs.push(keypair);
}
// Check all public keys are unique
let public_keys: Vec<String> = keypairs.iter().map(|kp| {
unsafe { CStr::from_ptr(kp.public_key).to_str().unwrap().to_string() }
}).collect();
for i in 0..public_keys.len() {
for j in (i+1)..public_keys.len() {
assert_ne!(public_keys[i], public_keys[j], "Keypairs should be unique");
}
}
// Cleanup
for keypair in &mut keypairs {
age_free_keypair(keypair);
}
}

View File

@@ -0,0 +1,88 @@
//! Complete FFI wrapper for the age encryption library.
//!
//! Provides C-compatible functions for all age encryption operations:
//! - Key generation (x25519, SSH)
//! - Encryption/decryption (memory and file-based)
//! - Passphrase-based encryption (scrypt)
//! - ASCII armor support
//! - Multiple recipients support
extern crate libc;
// Internal modules
mod helpers;
// Public modules
pub mod types;
pub mod keys;
pub mod encrypt;
pub mod decrypt;
pub mod passphrase;
pub mod file;
pub mod armor;
pub mod validation;
pub mod memory;
// Re-export all public types
pub use types::{AgeBuffer, AgeEncryptConfig, AgeKeypair, AgeResult};
// Re-export all public functions
pub use keys::{age_generate_keypair, age_generate_x25519, age_x25519_to_public};
pub use encrypt::{age_encrypt, age_encrypt_armor, age_encrypt_multi};
pub use decrypt::{age_decrypt, age_decrypt_multi, age_decrypt_ssh, age_decrypt_ssh_file};
pub use passphrase::{age_decrypt_passphrase, age_encrypt_passphrase};
pub use file::{
age_decrypt_file, age_decrypt_file_passphrase, age_decrypt_file_with_identity,
age_encrypt_to_file, age_encrypt_to_file_armor,
};
pub use armor::{age_armor, age_dearmor};
pub use validation::{
age_is_valid_ssh_recipient, age_is_valid_x25519_identity, age_is_valid_x25519_recipient,
age_recipient_type,
};
pub use memory::{age_free_buffer, age_free_keypair, age_free_string};
use std::os::raw::c_char;
/// Get the version of the age-ffi library.
/// Returns a static string, do not free.
#[no_mangle]
pub extern "C" fn age_version() -> *const c_char {
static VERSION: &[u8] = b"0.1.0\0";
VERSION.as_ptr() as *const c_char
}
/// Get the version of the underlying age library.
/// Returns a static string, do not free.
#[no_mangle]
pub extern "C" fn age_lib_version() -> *const c_char {
static VERSION: &[u8] = b"0.11.0\0";
VERSION.as_ptr() as *const c_char
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod keys_tests;
#[cfg(test)]
mod encrypt_tests;
#[cfg(test)]
mod decrypt_tests;
#[cfg(test)]
mod passphrase_tests;
#[cfg(test)]
mod armor_tests;
#[cfg(test)]
mod validation_tests;
#[cfg(test)]
mod memory_tests;
#[cfg(test)]
mod file_tests;

View File

@@ -0,0 +1,60 @@
//! Memory management functions.
use crate::types::{AgeBuffer, AgeKeypair};
use std::ffi::CString;
use std::os::raw::c_char;
/// Free a buffer allocated by this library.
///
/// # Safety
/// The buffer must have been allocated by one of the age_* functions.
#[no_mangle]
pub extern "C" fn age_free_buffer(buffer: *mut AgeBuffer) {
if buffer.is_null() {
return;
}
unsafe {
let buf = &*buffer;
if !buf.data.is_null() && buf.capacity > 0 {
// Reconstruct the boxed slice and drop it
let slice = std::slice::from_raw_parts_mut(buf.data, buf.capacity);
drop(Box::from_raw(slice as *mut [u8]));
}
(*buffer) = AgeBuffer::null();
}
}
/// Free a string allocated by this library.
///
/// # Safety
/// The pointer must have been allocated by one of the age_* functions.
#[no_mangle]
pub extern "C" fn age_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
/// Free a keypair allocated by age_generate_keypair.
///
/// # Safety
/// The keypair must have been allocated by age_generate_keypair.
#[no_mangle]
pub extern "C" fn age_free_keypair(keypair: *mut AgeKeypair) {
if keypair.is_null() {
return;
}
unsafe {
if !(*keypair).public_key.is_null() {
drop(CString::from_raw((*keypair).public_key));
}
if !(*keypair).private_key.is_null() {
drop(CString::from_raw((*keypair).private_key));
}
(*keypair) = AgeKeypair::null();
}
}

View File

@@ -0,0 +1,208 @@
//! Tests for memory management functions.
use crate::encrypt::*;
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use std::os::raw::c_char;
#[test]
fn test_free_buffer_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test message for buffer freeing";
let mut encrypted = AgeBuffer::null();
age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
// Should not crash
age_free_buffer(&mut encrypted);
// Buffer should be nulled out
assert!(encrypted.data.is_null());
assert_eq!(encrypted.len, 0);
assert_eq!(encrypted.capacity, 0);
age_free_keypair(&mut keypair);
}
#[test]
fn test_free_buffer_null() {
// Should not crash on null pointer
age_free_buffer(std::ptr::null_mut());
}
#[test]
fn test_free_buffer_already_null() {
let mut buffer = AgeBuffer::null();
// Should not crash on already-null buffer
age_free_buffer(&mut buffer);
}
#[test]
fn test_free_string_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test";
let mut armored: *mut c_char = std::ptr::null_mut();
crate::encrypt::age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
// Should not crash
age_free_string(armored);
age_free_keypair(&mut keypair);
}
#[test]
fn test_free_string_null() {
// Should not crash on null pointer
age_free_string(std::ptr::null_mut());
}
#[test]
fn test_free_keypair_basic() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Should not crash
age_free_keypair(&mut keypair);
// Keypair should be nulled out
assert!(keypair.public_key.is_null());
assert!(keypair.private_key.is_null());
}
#[test]
fn test_free_keypair_null() {
// Should not crash on null pointer
age_free_keypair(std::ptr::null_mut());
}
#[test]
fn test_free_keypair_already_null() {
let mut keypair = AgeKeypair::null();
// Should not crash on already-null keypair
age_free_keypair(&mut keypair);
}
#[test]
fn test_double_free_buffer() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Test";
let mut encrypted = AgeBuffer::null();
age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
age_free_buffer(&mut encrypted);
// Double free should be safe because we null out the pointer
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_double_free_keypair() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
age_free_keypair(&mut keypair);
// Double free should be safe because we null out the pointers
age_free_keypair(&mut keypair);
}
#[test]
fn test_multiple_allocations_and_frees() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Allocate and free multiple times
for _ in 0..100 {
let plaintext = b"Test message for repeated allocation";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
}
age_free_keypair(&mut keypair);
}
#[test]
fn test_large_allocation_and_free() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Allocate a large buffer (1MB)
let plaintext: Vec<u8> = vec![0x42; 1024 * 1024];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
assert!(encrypted.len > 1024 * 1024);
age_free_buffer(&mut encrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_age_buffer_from_vec() {
// Test the internal from_vec function
let vec = vec![1u8, 2, 3, 4, 5];
let buffer = AgeBuffer::from_vec(vec);
assert!(!buffer.data.is_null());
assert_eq!(buffer.len, 5);
assert_eq!(buffer.capacity, 5);
// Verify data
let slice = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len) };
assert_eq!(slice, &[1, 2, 3, 4, 5]);
// Clean up
let mut buffer = buffer;
age_free_buffer(&mut buffer);
}
#[test]
fn test_age_buffer_null() {
let buffer = AgeBuffer::null();
assert!(buffer.data.is_null());
assert_eq!(buffer.len, 0);
assert_eq!(buffer.capacity, 0);
}
#[test]
fn test_age_keypair_null() {
let keypair = AgeKeypair::null();
assert!(keypair.public_key.is_null());
assert!(keypair.private_key.is_null());
}

View File

@@ -0,0 +1,139 @@
//! Passphrase-based encryption and decryption (scrypt).
use crate::helpers::cstr_to_string;
use crate::types::{AgeBuffer, AgeResult};
use age::secrecy::SecretString;
use std::io::{Read, Write};
use std::os::raw::c_char;
/// Encrypt data using a passphrase.
///
/// # Arguments
/// * `plaintext` - Pointer to the plaintext data
/// * `plaintext_len` - Length of the plaintext
/// * `passphrase` - The passphrase string
/// * `armor` - If true, output will be ASCII-armored
/// * `output` - Pointer to receive the encrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_encrypt_passphrase(
plaintext: *const u8,
plaintext_len: usize,
passphrase: *const c_char,
armor: bool,
output: *mut AgeBuffer,
) -> AgeResult {
if plaintext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let plaintext = unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) };
let passphrase_str = match unsafe { cstr_to_string(passphrase) } {
Ok(s) => s,
Err(e) => return e,
};
let secret = SecretString::from(passphrase_str);
let encryptor = age::Encryptor::with_user_passphrase(secret);
let mut encrypted = Vec::new();
let result = if armor {
let armor_writer = age::armor::ArmoredWriter::wrap_output(&mut encrypted, age::armor::Format::AsciiArmor)
.map_err(|_| AgeResult::ArmorError);
match armor_writer {
Ok(armor) => {
match encryptor.wrap_output(armor) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
match writer.finish() {
Ok(armor) => armor.finish().map_err(|_| AgeResult::ArmorError),
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(_) => return AgeResult::EncryptionFailed,
}
}
Err(e) => return e,
}
} else {
match encryptor.wrap_output(&mut encrypted) {
Ok(mut writer) => {
if writer.write_all(plaintext).is_err() {
return AgeResult::EncryptionFailed;
}
writer.finish().map_err(|_| AgeResult::EncryptionFailed)
}
Err(_) => return AgeResult::EncryptionFailed,
}
};
if result.is_err() {
return AgeResult::EncryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(encrypted);
}
AgeResult::Success
}
/// Decrypt data using a passphrase.
///
/// # Arguments
/// * `ciphertext` - Pointer to the encrypted data
/// * `ciphertext_len` - Length of the ciphertext
/// * `passphrase` - The passphrase string
/// * `output` - Pointer to receive the decrypted buffer
///
/// # Returns
/// AgeResult indicating success or failure
#[no_mangle]
pub extern "C" fn age_decrypt_passphrase(
ciphertext: *const u8,
ciphertext_len: usize,
passphrase: *const c_char,
output: *mut AgeBuffer,
) -> AgeResult {
if ciphertext.is_null() || output.is_null() {
return AgeResult::InvalidInput;
}
let ciphertext = unsafe { std::slice::from_raw_parts(ciphertext, ciphertext_len) };
let passphrase_str = match unsafe { cstr_to_string(passphrase) } {
Ok(s) => s,
Err(e) => return e,
};
let secret = SecretString::from(passphrase_str);
let identity = age::scrypt::Identity::new(secret);
let decryptor = match age::Decryptor::new(ciphertext) {
Ok(d) => d,
Err(_) => return AgeResult::DecryptionFailed,
};
let mut decrypted = Vec::new();
let mut reader = match decryptor.decrypt(std::iter::once(&identity as &dyn age::Identity)) {
Ok(r) => r,
Err(_) => return AgeResult::DecryptionFailed,
};
if reader.read_to_end(&mut decrypted).is_err() {
return AgeResult::DecryptionFailed;
}
unsafe {
*output = AgeBuffer::from_vec(decrypted);
}
AgeResult::Success
}

View File

@@ -0,0 +1,329 @@
//! Tests for passphrase-based encryption and decryption.
use crate::passphrase::*;
use crate::armor::*;
use crate::memory::*;
use crate::types::*;
use std::ffi::CString;
#[test]
fn test_passphrase_encrypt_decrypt() {
let plaintext = b"Secret passphrase message";
let passphrase = CString::new("my-secure-passphrase").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_wrong_passphrase() {
let plaintext = b"Secret message";
let correct_passphrase = CString::new("correct-passphrase").unwrap();
let wrong_passphrase = CString::new("wrong-passphrase").unwrap();
let mut encrypted = AgeBuffer::null();
age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
correct_passphrase.as_ptr(),
false,
&mut encrypted,
);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
wrong_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
age_free_buffer(&mut encrypted);
}
#[test]
fn test_passphrase_empty_passphrase() {
let plaintext = b"Message with empty passphrase";
let empty_passphrase = CString::new("").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
empty_passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
empty_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_special_characters() {
let plaintext = b"Message with special passphrase";
let special_passphrase = CString::new("p@$$w0rd!#$%^&*()_+-=[]{}|;':\",./<>?").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
special_passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
special_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_with_armor() {
let plaintext = b"Armored passphrase message";
let passphrase = CString::new("armor-test-pass").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
true, // armor = true
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Verify it's armored
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
let encrypted_str = std::str::from_utf8(encrypted_slice).unwrap();
assert!(encrypted_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
// Dearmor first
let armored_cstr = CString::new(encrypted_str).unwrap();
let mut dearmored = AgeBuffer::null();
age_dearmor(armored_cstr.as_ptr(), &mut dearmored);
// Then decrypt
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
dearmored.data,
dearmored.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_null_input() {
let passphrase = CString::new("test").unwrap();
let mut output = AgeBuffer::null();
// Null plaintext
let result = age_encrypt_passphrase(
std::ptr::null(),
0,
passphrase.as_ptr(),
false,
&mut output,
);
assert_eq!(result, AgeResult::InvalidInput);
// Null output
let plaintext = b"test";
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_passphrase_long_passphrase() {
let plaintext = b"Message with very long passphrase";
// 1000 character passphrase
let long_passphrase = CString::new("a".repeat(1000)).unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
long_passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
long_passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_passphrase_encrypt_null_passphrase() {
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
std::ptr::null(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_passphrase_decrypt_null_passphrase() {
let passphrase = CString::new("test").unwrap();
let plaintext = b"test";
// First encrypt with valid passphrase
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Try to decrypt with null passphrase
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
std::ptr::null(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
}
#[test]
fn test_passphrase_decrypt_null_output() {
let passphrase = CString::new("test").unwrap();
let plaintext = b"test";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Try to decrypt with null output
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
passphrase.as_ptr(),
std::ptr::null_mut(),
);
assert_eq!(result, AgeResult::InvalidInput);
age_free_buffer(&mut encrypted);
}
#[test]
fn test_passphrase_decrypt_null_ciphertext() {
let passphrase = CString::new("test").unwrap();
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
std::ptr::null(),
0,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::InvalidInput);
}
#[test]
fn test_passphrase_decrypt_corrupted_data() {
let passphrase = CString::new("test").unwrap();
let corrupted = b"not valid encrypted data";
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
corrupted.as_ptr(),
corrupted.len(),
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::DecryptionFailed);
}

View File

@@ -0,0 +1,337 @@
//! Tests for the age-ffi library.
use crate::*;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[test]
fn test_keygen() {
let mut keypair = AgeKeypair::null();
let result = age_generate_x25519(&mut keypair);
assert_eq!(result, AgeResult::Success);
assert!(!keypair.public_key.is_null());
assert!(!keypair.private_key.is_null());
unsafe {
let public = CStr::from_ptr(keypair.public_key).to_str().unwrap();
let private = CStr::from_ptr(keypair.private_key).to_str().unwrap();
assert!(public.starts_with("age1"));
assert!(private.starts_with("AGE-SECRET-KEY-1"));
}
age_free_keypair(&mut keypair);
}
#[test]
fn test_encrypt_decrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Hello, world!";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
assert!(!encrypted.data.is_null());
assert!(encrypted.len > 0);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_passphrase_encrypt_decrypt() {
let plaintext = b"Secret message";
let passphrase = CString::new("my-secret-passphrase").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
encrypted.data,
encrypted.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_validation() {
let invalid = CString::new("not-a-key").unwrap();
assert!(!age_is_valid_x25519_recipient(invalid.as_ptr()));
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(age_is_valid_x25519_recipient(keypair.public_key));
assert!(age_is_valid_x25519_identity(keypair.private_key));
assert_eq!(age_recipient_type(keypair.public_key), 1);
age_free_keypair(&mut keypair);
}
#[test]
fn test_armor_encrypt_decrypt() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"Armored message";
let mut armored: *mut c_char = std::ptr::null_mut();
let result = age_encrypt_armor(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut armored,
);
assert_eq!(result, AgeResult::Success);
assert!(!armored.is_null());
let armored_str = unsafe { CStr::from_ptr(armored).to_str().unwrap() };
assert!(armored_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored, &mut dearmored);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
dearmored.data,
dearmored.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_string(armored);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_derive_public_key() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let mut derived_public: *mut c_char = std::ptr::null_mut();
let result = age_x25519_to_public(keypair.private_key, &mut derived_public);
assert_eq!(result, AgeResult::Success);
let original = unsafe { CStr::from_ptr(keypair.public_key).to_str().unwrap() };
let derived = unsafe { CStr::from_ptr(derived_public).to_str().unwrap() };
assert_eq!(original, derived);
age_free_string(derived_public);
age_free_keypair(&mut keypair);
}
#[test]
fn test_multi_recipient_encrypt() {
let mut keypair1 = AgeKeypair::null();
let mut keypair2 = AgeKeypair::null();
age_generate_x25519(&mut keypair1);
age_generate_x25519(&mut keypair2);
let plaintext = b"Message for multiple recipients";
let recipients: [*const c_char; 2] = [
keypair1.public_key as *const c_char,
keypair2.public_key as *const c_char,
];
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_multi(
plaintext.as_ptr(),
plaintext.len(),
recipients.as_ptr(),
recipients.len(),
false,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Decrypt with first key
let mut decrypted1 = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair1.private_key,
&mut decrypted1,
);
assert_eq!(result, AgeResult::Success);
let slice1 = unsafe { std::slice::from_raw_parts(decrypted1.data, decrypted1.len) };
assert_eq!(slice1, plaintext);
// Decrypt with second key
let mut decrypted2 = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair2.private_key,
&mut decrypted2,
);
assert_eq!(result, AgeResult::Success);
let slice2 = unsafe { std::slice::from_raw_parts(decrypted2.data, decrypted2.len) };
assert_eq!(slice2, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted1);
age_free_buffer(&mut decrypted2);
age_free_keypair(&mut keypair1);
age_free_keypair(&mut keypair2);
}
#[test]
fn test_version_functions() {
let version = age_version();
assert!(!version.is_null());
let version_str = unsafe { CStr::from_ptr(version).to_str().unwrap() };
assert!(!version_str.is_empty());
let lib_version = age_lib_version();
assert!(!lib_version.is_null());
let lib_version_str = unsafe { CStr::from_ptr(lib_version).to_str().unwrap() };
assert!(lib_version_str.starts_with("0.11"));
}
#[test]
fn test_passphrase_with_armor() {
let plaintext = b"Armored passphrase message";
let passphrase = CString::new("test-passphrase-123").unwrap();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt_passphrase(
plaintext.as_ptr(),
plaintext.len(),
passphrase.as_ptr(),
true, // armor = true
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
// Verify it's armored
let encrypted_slice = unsafe { std::slice::from_raw_parts(encrypted.data, encrypted.len) };
let encrypted_str = std::str::from_utf8(encrypted_slice).unwrap();
assert!(encrypted_str.contains("-----BEGIN AGE ENCRYPTED FILE-----"));
// Dearmor first, then decrypt
let armored_cstr = CString::new(encrypted_str).unwrap();
let mut dearmored = AgeBuffer::null();
let result = age_dearmor(armored_cstr.as_ptr(), &mut dearmored);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt_passphrase(
dearmored.data,
dearmored.len,
passphrase.as_ptr(),
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut dearmored);
age_free_buffer(&mut decrypted);
}
#[test]
fn test_empty_plaintext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
let plaintext = b"";
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
assert_eq!(decrypted.len, 0);
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}
#[test]
fn test_large_plaintext() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// 1MB of data
let plaintext: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
let mut encrypted = AgeBuffer::null();
let result = age_encrypt(
plaintext.as_ptr(),
plaintext.len(),
keypair.public_key,
&mut encrypted,
);
assert_eq!(result, AgeResult::Success);
let mut decrypted = AgeBuffer::null();
let result = age_decrypt(
encrypted.data,
encrypted.len,
keypair.private_key,
&mut decrypted,
);
assert_eq!(result, AgeResult::Success);
let decrypted_slice = unsafe { std::slice::from_raw_parts(decrypted.data, decrypted.len) };
assert_eq!(decrypted_slice, plaintext.as_slice());
age_free_buffer(&mut encrypted);
age_free_buffer(&mut decrypted);
age_free_keypair(&mut keypair);
}

View File

@@ -0,0 +1,92 @@
//! FFI-compatible data types for the age encryption library.
use std::os::raw::c_char;
/// Result codes for FFI functions
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgeResult {
Success = 0,
InvalidInput = 1,
EncryptionFailed = 2,
DecryptionFailed = 3,
KeygenFailed = 4,
IoError = 5,
InvalidRecipient = 6,
InvalidIdentity = 7,
NoRecipients = 8,
NoIdentities = 9,
ArmorError = 10,
PassphraseRequired = 11,
InvalidPassphrase = 12,
SshKeyError = 13,
MemoryAllocationFailed = 14,
InvalidUtf8 = 15,
UnsupportedKey = 16,
}
/// A buffer containing binary data allocated by the library.
/// Caller must free using age_free_buffer.
#[repr(C)]
pub struct AgeBuffer {
pub data: *mut u8,
pub len: usize,
pub capacity: usize,
}
impl AgeBuffer {
pub fn null() -> Self {
AgeBuffer {
data: std::ptr::null_mut(),
len: 0,
capacity: 0,
}
}
pub fn from_vec(v: Vec<u8>) -> Self {
let mut v = v.into_boxed_slice();
let data = v.as_mut_ptr();
let len = v.len();
std::mem::forget(v);
AgeBuffer {
data,
len,
capacity: len,
}
}
}
/// A keypair containing public and private keys as C strings.
/// Caller must free both strings using age_free_string.
#[repr(C)]
pub struct AgeKeypair {
pub public_key: *mut c_char,
pub private_key: *mut c_char,
}
impl AgeKeypair {
pub fn null() -> Self {
AgeKeypair {
public_key: std::ptr::null_mut(),
private_key: std::ptr::null_mut(),
}
}
}
/// Configuration for encryption operations.
#[repr(C)]
pub struct AgeEncryptConfig {
/// If true, output will be ASCII-armored
pub armor: bool,
/// Work factor for scrypt (0 = default, typically 18-22)
pub scrypt_work_factor: u8,
}
impl Default for AgeEncryptConfig {
fn default() -> Self {
AgeEncryptConfig {
armor: false,
scrypt_work_factor: 0,
}
}
}

View File

@@ -0,0 +1,81 @@
//! Recipient and identity validation functions.
use crate::helpers::cstr_to_str;
use std::os::raw::c_char;
use std::str::FromStr;
/// Check if a string is a valid x25519 recipient (public key).
///
/// # Arguments
/// * `recipient` - The recipient string to validate
///
/// # Returns
/// true if valid, false otherwise
#[no_mangle]
pub extern "C" fn age_is_valid_x25519_recipient(recipient: *const c_char) -> bool {
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(_) => return false,
};
recipient_str.parse::<age::x25519::Recipient>().is_ok()
}
/// Check if a string is a valid x25519 identity (private key).
///
/// # Arguments
/// * `identity` - The identity string to validate
///
/// # Returns
/// true if valid, false otherwise
#[no_mangle]
pub extern "C" fn age_is_valid_x25519_identity(identity: *const c_char) -> bool {
let identity_str = match unsafe { cstr_to_str(identity) } {
Ok(s) => s,
Err(_) => return false,
};
age::x25519::Identity::from_str(identity_str).is_ok()
}
/// Check if a string is a valid SSH recipient (public key).
///
/// # Arguments
/// * `recipient` - The recipient string to validate
///
/// # Returns
/// true if valid, false otherwise
#[no_mangle]
pub extern "C" fn age_is_valid_ssh_recipient(recipient: *const c_char) -> bool {
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s,
Err(_) => return false,
};
recipient_str.parse::<age::ssh::Recipient>().is_ok()
}
/// Get the type of a recipient string.
///
/// # Arguments
/// * `recipient` - The recipient string
///
/// # Returns
/// 0 = invalid, 1 = x25519, 2 = ssh (ed25519 or rsa)
#[no_mangle]
pub extern "C" fn age_recipient_type(recipient: *const c_char) -> u8 {
let recipient_str = match unsafe { cstr_to_str(recipient) } {
Ok(s) => s.trim(),
Err(_) => return 0,
};
if recipient_str.parse::<age::x25519::Recipient>().is_ok() {
return 1;
}
if recipient_str.parse::<age::ssh::Recipient>().is_ok() {
return 2;
}
0
}

View File

@@ -0,0 +1,133 @@
//! Tests for recipient and identity validation functions.
use crate::keys::*;
use crate::memory::*;
use crate::types::*;
use crate::validation::*;
use std::ffi::CString;
#[test]
fn test_is_valid_x25519_recipient_valid() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(age_is_valid_x25519_recipient(keypair.public_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_recipient_invalid() {
let invalid = CString::new("not-a-valid-key").unwrap();
assert!(!age_is_valid_x25519_recipient(invalid.as_ptr()));
let almost_valid = CString::new("age1qqqqqqqqqqqqqqqqqqqqq").unwrap();
assert!(!age_is_valid_x25519_recipient(almost_valid.as_ptr()));
// Private key should not be valid as recipient
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(!age_is_valid_x25519_recipient(keypair.private_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_recipient_null() {
assert!(!age_is_valid_x25519_recipient(std::ptr::null()));
}
#[test]
fn test_is_valid_x25519_identity_valid() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(age_is_valid_x25519_identity(keypair.private_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_identity_invalid() {
let invalid = CString::new("not-a-valid-key").unwrap();
assert!(!age_is_valid_x25519_identity(invalid.as_ptr()));
let almost_valid = CString::new("AGE-SECRET-KEY-1QQQQQQQQQQQQQ").unwrap();
assert!(!age_is_valid_x25519_identity(almost_valid.as_ptr()));
// Public key should not be valid as identity
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert!(!age_is_valid_x25519_identity(keypair.public_key));
age_free_keypair(&mut keypair);
}
#[test]
fn test_is_valid_x25519_identity_null() {
assert!(!age_is_valid_x25519_identity(std::ptr::null()));
}
#[test]
fn test_is_valid_ssh_recipient() {
// Test with an ed25519 SSH public key format
let ed25519_key = CString::new("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGXzDvU2fB2Z9R7z1q1q1q1q1q1q1q1q1q1q1q1q1q1q").unwrap();
// This might or might not be valid depending on exact format
// The important thing is the function doesn't crash
let _ = age_is_valid_ssh_recipient(ed25519_key.as_ptr());
// Invalid SSH key
let invalid = CString::new("not-an-ssh-key").unwrap();
assert!(!age_is_valid_ssh_recipient(invalid.as_ptr()));
}
#[test]
fn test_is_valid_ssh_recipient_null() {
assert!(!age_is_valid_ssh_recipient(std::ptr::null()));
}
#[test]
fn test_recipient_type_x25519() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
assert_eq!(age_recipient_type(keypair.public_key), 1);
age_free_keypair(&mut keypair);
}
#[test]
fn test_recipient_type_invalid() {
let invalid = CString::new("not-a-valid-key").unwrap();
assert_eq!(age_recipient_type(invalid.as_ptr()), 0);
}
#[test]
fn test_recipient_type_null() {
assert_eq!(age_recipient_type(std::ptr::null()), 0);
}
#[test]
fn test_recipient_type_with_whitespace() {
let mut keypair = AgeKeypair::null();
age_generate_x25519(&mut keypair);
// Get the public key and add whitespace
let public_key_str = unsafe {
std::ffi::CStr::from_ptr(keypair.public_key).to_str().unwrap()
};
let with_whitespace = CString::new(format!(" {} ", public_key_str)).unwrap();
// Should still be recognized as x25519 after trimming
assert_eq!(age_recipient_type(with_whitespace.as_ptr()), 1);
age_free_keypair(&mut keypair);
}
#[test]
fn test_empty_string_validation() {
let empty = CString::new("").unwrap();
assert!(!age_is_valid_x25519_recipient(empty.as_ptr()));
assert!(!age_is_valid_x25519_identity(empty.as_ptr()));
assert!(!age_is_valid_ssh_recipient(empty.as_ptr()));
assert_eq!(age_recipient_type(empty.as_ptr()), 0);
}

View File

@@ -0,0 +1,243 @@
# Age-FFI Zig Bindings
Idiomatic Zig bindings for the [age](https://age-encryption.org/) encryption library.
## Features
- **Complete FFI coverage** - All age-ffi functions exposed
- **Memory safety** - RAII wrappers with automatic cleanup
- **Idiomatic error handling** - Zig errors instead of C result codes
- **Type safety** - Strong typing with Zig's type system
- **Easy to use** - High-level API that feels native to Zig
## Building the C Library
First, build the Rust FFI library:
```bash
cd ..
cargo build --release
```
This creates a static library at `../target/release/libage_ffi.a`.
## Using the Bindings
### In Your Build Script
```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "my-app",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Add the age module
const age_module = b.addModule("age", .{
.root_source_file = .{ .path = "path/to/age-ffi/zig/age.zig" },
});
exe.root_module.addImport("age", age_module);
// Link the static library
exe.addLibraryPath(.{ .path = "path/to/age-ffi/target/release" });
exe.linkSystemLibrary("age_ffi");
exe.linkLibC();
b.installArtifact(exe);
}
```
### In Your Code
```zig
const age = @import("age");
// Generate a keypair
var keypair = try age.generateKeypair();
defer keypair.deinit();
// Encrypt
const plaintext = "Secret message";
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
defer encrypted.deinit();
// Decrypt
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
defer decrypted.deinit();
```
## API Overview
### Key Generation
```zig
// Generate new keypair
var keypair = try age.generateKeypair();
defer keypair.deinit();
// Derive public key from private key
const public_key = try age.derivePublicKey(allocator, private_key);
defer allocator.free(public_key);
```
### Encryption
```zig
// Simple encryption
var encrypted = try age.encrypt(plaintext, recipient);
defer encrypted.deinit();
// With ASCII armor
var armored = try age.encryptArmor(plaintext, recipient);
defer armored.deinit();
// Multiple recipients
const recipients = [_][:0]const u8{ recipient1, recipient2 };
var multi = try age.encryptMulti(plaintext, &recipients, false);
defer multi.deinit();
// Passphrase-based
var pass_enc = try age.encryptPassphrase(plaintext, passphrase, true);
defer pass_enc.deinit();
```
### Decryption
```zig
// Simple decryption
var decrypted = try age.decrypt(ciphertext, identity);
defer decrypted.deinit();
// With multiple identities (tries each)
const identities = [_][:0]const u8{ id1, id2 };
var multi = try age.decryptMulti(ciphertext, &identities);
defer multi.deinit();
// SSH key support
var ssh_dec = try age.decryptSsh(ciphertext, ssh_private_key);
defer ssh_dec.deinit();
// Passphrase-based
var pass_dec = try age.decryptPassphrase(ciphertext, passphrase);
defer pass_dec.deinit();
```
### File Operations
```zig
// Encrypt to file
try age.encryptToFileArmor(plaintext, recipient, "/path/to/file.age");
// Decrypt from file
var decrypted = try age.decryptFileWithIdentity("/path/to/file.age", identity);
defer decrypted.deinit();
```
### Validation
```zig
// Validate keys
const is_valid = age.isValidX25519Recipient(recipient);
// Check recipient type
const recipient_type = age.getRecipientType(recipient);
// Returns: .invalid, .x25519, or .ssh
```
### ASCII Armor
```zig
// Add armor
var armored = try age.armor(binary_data);
defer armored.deinit();
// Remove armor
var binary = try age.dearmor(armored_data);
defer binary.deinit();
```
## Memory Management
The bindings use RAII wrappers that automatically free resources:
- `Buffer` - Wraps `AgeBuffer`, freed on `deinit()`
- `Keypair` - Wraps `AgeKeypair`, freed on `deinit()`
- `CString` - Wraps C strings, freed on `deinit()`
Always call `defer x.deinit()` after creating these objects.
## Error Handling
All operations return `AgeError!T` with the following error types:
- `InvalidInput`
- `EncryptionFailed`
- `DecryptionFailed`
- `KeygenFailed`
- `IoError`
- `InvalidRecipient`
- `InvalidIdentity`
- `NoRecipients`
- `NoIdentities`
- `ArmorError`
- `PassphraseRequired`
- `InvalidPassphrase`
- `SshKeyError`
- `MemoryAllocationFailed`
- `InvalidUtf8`
- `UnsupportedKey`
## Example
See `example.zig` for a comprehensive demonstration of all features.
Run the example:
```bash
# Build the example (requires build.zig in this directory)
zig build-exe example.zig -I.. -L../target/release -lage_ffi -lc
# Or manually:
zig build-exe example.zig \
-I.. \
-L../target/release \
-lage_ffi \
-lc
./example
```
## Low-Level C API
The module also exposes the raw C functions if you need direct FFI access:
```zig
const result = age.age_encrypt(
plaintext.ptr,
plaintext.len,
recipient.ptr,
&output,
);
```
## Version Information
```zig
const version = age.getVersion(); // age-ffi version
const lib_version = age.getLibVersion(); // underlying age library version
```
## Safety Notes
1. All C strings must be null-terminated (`:0` sentinel)
2. Buffers returned by the library must be freed with `deinit()`
3. Don't use buffers after calling `deinit()`
4. The `toOwnedSlice()` method transfers ownership and calls `deinit()` automatically
## License
Same as the parent age-ffi project.

View File

@@ -0,0 +1,631 @@
//! Zig bindings for the age-ffi library
//!
//! This module provides idiomatic Zig wrappers around the age encryption library's C FFI.
//! It handles memory management, error conversion, and provides safe interfaces.
const std = @import("std");
const c = @cImport({});
// ============================================================================
// C Types and Structures
// ============================================================================
/// Result codes for FFI functions
pub const AgeResult = enum(c_int) {
success = 0,
invalid_input = 1,
encryption_failed = 2,
decryption_failed = 3,
keygen_failed = 4,
io_error = 5,
invalid_recipient = 6,
invalid_identity = 7,
no_recipients = 8,
no_identities = 9,
armor_error = 10,
passphrase_required = 11,
invalid_passphrase = 12,
ssh_key_error = 13,
memory_allocation_failed = 14,
invalid_utf8 = 15,
unsupported_key = 16,
};
/// A buffer containing binary data allocated by the library.
/// Caller must free using age_free_buffer.
pub const AgeBuffer = extern struct {
data: [*]u8,
len: usize,
capacity: usize,
pub fn toSlice(self: AgeBuffer) []u8 {
return self.data[0..self.len];
}
};
/// A keypair containing public and private keys as C strings.
/// Caller must free using age_free_keypair.
pub const AgeKeypair = extern struct {
public_key: [*:0]u8,
private_key: [*:0]u8,
pub fn getPublicKey(self: AgeKeypair) [:0]const u8 {
return std.mem.span(self.public_key);
}
pub fn getPrivateKey(self: AgeKeypair) [:0]const u8 {
return std.mem.span(self.private_key);
}
};
/// Configuration for encryption operations.
pub const AgeEncryptConfig = extern struct {
armor: bool,
scrypt_work_factor: u8,
pub fn default() AgeEncryptConfig {
return .{
.armor = false,
.scrypt_work_factor = 0,
};
}
};
// ============================================================================
// Raw C FFI Declarations
// ============================================================================
/// Get the version of the age-ffi library (static string, do not free)
pub extern "C" fn age_version() [*:0]const u8;
/// Get the version of the underlying age library (static string, do not free)
pub extern "C" fn age_lib_version() [*:0]const u8;
// Key generation
pub extern "C" fn age_generate_x25519(keypair: *AgeKeypair) AgeResult;
pub extern "C" fn age_generate_keypair(keypair: *AgeKeypair) AgeResult;
pub extern "C" fn age_x25519_to_public(private_key: [*:0]const u8, public_key: *[*:0]u8) AgeResult;
// Encryption
pub extern "C" fn age_encrypt(
plaintext: [*]const u8,
plaintext_len: usize,
recipient: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_encrypt_multi(
plaintext: [*]const u8,
plaintext_len: usize,
recipients: [*]const [*:0]const u8,
recipient_count: usize,
armor: bool,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_encrypt_armor(
plaintext: [*]const u8,
plaintext_len: usize,
recipient: [*:0]const u8,
output: *[*:0]u8,
) AgeResult;
// Decryption
pub extern "C" fn age_decrypt(
ciphertext: [*]const u8,
ciphertext_len: usize,
identity: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_decrypt_multi(
ciphertext: [*]const u8,
ciphertext_len: usize,
identities: [*]const [*:0]const u8,
identity_count: usize,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_decrypt_ssh(
ciphertext: [*]const u8,
ciphertext_len: usize,
ssh_key: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_decrypt_ssh_file(
ciphertext: [*]const u8,
ciphertext_len: usize,
ssh_key_path: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
// Passphrase
pub extern "C" fn age_encrypt_passphrase(
plaintext: [*]const u8,
plaintext_len: usize,
passphrase: [*:0]const u8,
armor: bool,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_decrypt_passphrase(
ciphertext: [*]const u8,
ciphertext_len: usize,
passphrase: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
// File operations
pub extern "C" fn age_encrypt_to_file(
plaintext: [*]const u8,
plaintext_len: usize,
output_path: [*:0]const u8,
recipient: [*:0]const u8,
) AgeResult;
pub extern "C" fn age_encrypt_to_file_armor(
plaintext: [*]const u8,
plaintext_len: usize,
output_path: [*:0]const u8,
recipient: [*:0]const u8,
) AgeResult;
pub extern "C" fn age_decrypt_file(
input_path: [*:0]const u8,
identity_path: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_decrypt_file_with_identity(
input_path: [*:0]const u8,
identity: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
pub extern "C" fn age_decrypt_file_passphrase(
input_path: [*:0]const u8,
passphrase: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
// Armor
pub extern "C" fn age_armor(
data: [*]const u8,
data_len: usize,
output: *[*:0]u8,
) AgeResult;
pub extern "C" fn age_dearmor(
armored: [*:0]const u8,
output: *AgeBuffer,
) AgeResult;
// Validation
pub extern "C" fn age_is_valid_x25519_recipient(recipient: [*:0]const u8) bool;
pub extern "C" fn age_is_valid_x25519_identity(identity: [*:0]const u8) bool;
pub extern "C" fn age_is_valid_ssh_recipient(recipient: [*:0]const u8) bool;
pub extern "C" fn age_recipient_type(recipient: [*:0]const u8) c_int;
// Memory management
pub extern "C" fn age_free_buffer(buffer: *AgeBuffer) void;
pub extern "C" fn age_free_string(s: [*:0]u8) void;
pub extern "C" fn age_free_keypair(keypair: *AgeKeypair) void;
// ============================================================================
// Error Handling
// ============================================================================
pub const AgeError = error{
InvalidInput,
EncryptionFailed,
DecryptionFailed,
KeygenFailed,
IoError,
InvalidRecipient,
InvalidIdentity,
NoRecipients,
NoIdentities,
ArmorError,
PassphraseRequired,
InvalidPassphrase,
SshKeyError,
MemoryAllocationFailed,
InvalidUtf8,
UnsupportedKey,
};
fn resultToError(result: AgeResult) AgeError!void {
return switch (result) {
.success => {},
.invalid_input => AgeError.InvalidInput,
.encryption_failed => AgeError.EncryptionFailed,
.decryption_failed => AgeError.DecryptionFailed,
.keygen_failed => AgeError.KeygenFailed,
.io_error => AgeError.IoError,
.invalid_recipient => AgeError.InvalidRecipient,
.invalid_identity => AgeError.InvalidIdentity,
.no_recipients => AgeError.NoRecipients,
.no_identities => AgeError.NoIdentities,
.armor_error => AgeError.ArmorError,
.passphrase_required => AgeError.PassphraseRequired,
.invalid_passphrase => AgeError.InvalidPassphrase,
.ssh_key_error => AgeError.SshKeyError,
.memory_allocation_failed => AgeError.MemoryAllocationFailed,
.invalid_utf8 => AgeError.InvalidUtf8,
.unsupported_key => AgeError.UnsupportedKey,
};
}
// ============================================================================
// RAII Wrappers for Memory Management
// ============================================================================
/// RAII wrapper for AgeBuffer that automatically frees on deinit
pub const Buffer = struct {
buffer: AgeBuffer,
pub fn deinit(self: *Buffer) void {
age_free_buffer(&self.buffer);
}
pub fn toSlice(self: Buffer) []u8 {
return self.buffer.toSlice();
}
pub fn toOwnedSlice(self: *Buffer, allocator: std.mem.Allocator) ![]u8 {
const slice = try allocator.dupe(u8, self.buffer.toSlice());
self.deinit();
return slice;
}
};
/// RAII wrapper for AgeKeypair that automatically frees on deinit
pub const Keypair = struct {
keypair: AgeKeypair,
pub fn deinit(self: *Keypair) void {
age_free_keypair(&self.keypair);
}
pub fn getPublicKey(self: Keypair) [:0]const u8 {
return self.keypair.getPublicKey();
}
pub fn getPrivateKey(self: Keypair) [:0]const u8 {
return self.keypair.getPrivateKey();
}
};
/// RAII wrapper for C strings that automatically frees on deinit
pub const CString = struct {
ptr: [*:0]u8,
pub fn deinit(self: CString) void {
age_free_string(self.ptr);
}
pub fn slice(self: CString) [:0]const u8 {
return std.mem.span(self.ptr);
}
};
// ============================================================================
// High-Level Idiomatic Zig API
// ============================================================================
/// Get library version information
pub fn getVersion() [:0]const u8 {
return std.mem.span(age_version());
}
/// Get underlying age library version
pub fn getLibVersion() [:0]const u8 {
return std.mem.span(age_lib_version());
}
/// Generate a new x25519 keypair
pub fn generateKeypair() AgeError!Keypair {
var keypair: AgeKeypair = undefined;
const result = age_generate_x25519(&keypair);
try resultToError(result);
return Keypair{ .keypair = keypair };
}
/// Derive public key from a private x25519 identity
pub fn derivePublicKey(allocator: std.mem.Allocator, private_key: [:0]const u8) (AgeError || error{OutOfMemory})![]u8 {
var public_key: [*:0]u8 = undefined;
const result = age_x25519_to_public(private_key.ptr, &public_key);
try resultToError(result);
defer age_free_string(public_key);
return allocator.dupe(u8, std.mem.span(public_key));
}
/// Encrypt data with a single recipient
pub fn encrypt(plaintext: []const u8, recipient: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_encrypt(
plaintext.ptr,
plaintext.len,
recipient.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Encrypt data with multiple recipients
pub fn encryptMulti(plaintext: []const u8, recipients: []const [:0]const u8, use_armor: bool) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
// Convert Zig sentinel-terminated slices to C pointers
// We need to build an array of [*:0]const u8 pointers
var ptrs_buf: [16][*:0]const u8 = undefined;
if (recipients.len > ptrs_buf.len) return AgeError.NoRecipients;
for (recipients, 0..) |recip, i| {
ptrs_buf[i] = recip.ptr;
}
const result = age_encrypt_multi(
plaintext.ptr,
plaintext.len,
&ptrs_buf,
recipients.len,
use_armor,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Encrypt data with ASCII armor (returns armored string as bytes)
pub fn encryptArmor(plaintext: []const u8, recipient: [:0]const u8) AgeError!Buffer {
var c_output: [*:0]u8 = undefined;
const result = age_encrypt_armor(
plaintext.ptr,
plaintext.len,
recipient.ptr,
&c_output,
);
try resultToError(result);
// Convert C string to buffer
const str = std.mem.span(c_output);
const output: AgeBuffer = .{
.data = c_output,
.len = str.len,
.capacity = str.len,
};
return Buffer{ .buffer = output };
}
/// Decrypt data with a single identity
pub fn decrypt(ciphertext: []const u8, identity: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt(
ciphertext.ptr,
ciphertext.len,
identity.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt data with multiple identities (tries each until one succeeds)
pub fn decryptMulti(ciphertext: []const u8, identities: []const [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
// Convert Zig sentinel-terminated slices to C pointers
// We need to build an array of [*:0]const u8 pointers
var ptrs_buf: [16][*:0]const u8 = undefined;
if (identities.len > ptrs_buf.len) return AgeError.NoIdentities;
for (identities, 0..) |ident, i| {
ptrs_buf[i] = ident.ptr;
}
const result = age_decrypt_multi(
ciphertext.ptr,
ciphertext.len,
&ptrs_buf,
identities.len,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt using an SSH private key (from string)
pub fn decryptSsh(ciphertext: []const u8, ssh_key: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt_ssh(
ciphertext.ptr,
ciphertext.len,
ssh_key.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt using an SSH private key file
pub fn decryptSshFile(ciphertext: []const u8, ssh_key_path: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt_ssh_file(
ciphertext.ptr,
ciphertext.len,
ssh_key_path.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Encrypt with a passphrase
pub fn encryptPassphrase(plaintext: []const u8, passphrase: [:0]const u8, use_armor: bool) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_encrypt_passphrase(
plaintext.ptr,
plaintext.len,
passphrase.ptr,
use_armor,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt with a passphrase
/// Note: If the data is ASCII-armored, you must dearmor it first using dearmor()
/// or use decryptPassphraseArmored() for convenience.
pub fn decryptPassphrase(ciphertext: []const u8, passphrase: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt_passphrase(
ciphertext.ptr,
ciphertext.len,
passphrase.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt armored passphrase-encrypted data (convenience function)
/// Automatically dearmors the data before decryption.
pub fn decryptPassphraseArmored(armored: []const u8, passphrase: [:0]const u8) AgeError!Buffer {
// First dearmor the data
var dearmored = try dearmor(armored);
defer dearmored.deinit();
// Then decrypt the binary data
return try decryptPassphrase(dearmored.toSlice(), passphrase);
}
/// Encrypt data to a file
pub fn encryptToFile(plaintext: []const u8, recipient: [:0]const u8, output_path: [:0]const u8) AgeError!void {
const result = age_encrypt_to_file(
plaintext.ptr,
plaintext.len,
output_path.ptr,
recipient.ptr,
);
try resultToError(result);
}
/// Encrypt data to a file with ASCII armor
pub fn encryptToFileArmor(plaintext: []const u8, recipient: [:0]const u8, output_path: [:0]const u8) AgeError!void {
const result = age_encrypt_to_file_armor(
plaintext.ptr,
plaintext.len,
output_path.ptr,
recipient.ptr,
);
try resultToError(result);
}
/// Decrypt from a file using an identity file
pub fn decryptFile(input_path: [:0]const u8, identity_path: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt_file(
input_path.ptr,
identity_path.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt from a file using an identity string
pub fn decryptFileWithIdentity(input_path: [:0]const u8, identity: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt_file_with_identity(
input_path.ptr,
identity.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Decrypt from a file using a passphrase
pub fn decryptFilePassphrase(input_path: [:0]const u8, passphrase: [:0]const u8) AgeError!Buffer {
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_decrypt_file_passphrase(
input_path.ptr,
passphrase.ptr,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
/// Wrap binary data in ASCII armor (returns armored string as bytes)
pub fn armor(data: []const u8) AgeError!Buffer {
var c_output: [*:0]u8 = undefined;
const result = age_armor(
data.ptr,
data.len,
&c_output,
);
try resultToError(result);
// Convert C string to buffer
const str = std.mem.span(c_output);
const output: AgeBuffer = .{
.data = c_output,
.len = str.len,
.capacity = str.len,
};
return Buffer{ .buffer = output };
}
/// Remove ASCII armor from armored string
pub fn dearmor(armored: []const u8) AgeError!Buffer {
// Need to ensure the armored data is null-terminated
// Since it's coming from armor() it should be, but we need to treat it as a C string
const c_armored: [*:0]const u8 = @ptrCast(armored.ptr);
var output: AgeBuffer = .{ .data = undefined, .len = 0, .capacity = 0 };
const result = age_dearmor(
c_armored,
&output,
);
try resultToError(result);
return Buffer{ .buffer = output };
}
// ============================================================================
// Validation Functions
// ============================================================================
/// Validate an x25519 recipient (public key)
pub fn isValidX25519Recipient(recipient: [:0]const u8) bool {
return age_is_valid_x25519_recipient(recipient.ptr);
}
/// Validate an x25519 identity (private key)
pub fn isValidX25519Identity(identity: [:0]const u8) bool {
return age_is_valid_x25519_identity(identity.ptr);
}
/// Validate an SSH recipient
pub fn isValidSshRecipient(recipient: [:0]const u8) bool {
return age_is_valid_ssh_recipient(recipient.ptr);
}
/// Identify recipient type (0=invalid, 1=x25519, 2=ssh)
pub const RecipientType = enum(c_int) {
invalid = 0,
x25519 = 1,
ssh = 2,
};
pub fn getRecipientType(recipient: [:0]const u8) RecipientType {
const type_code = age_recipient_type(recipient.ptr);
return @enumFromInt(type_code);
}

View File

@@ -0,0 +1,89 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Create the age module
const age_module = b.addModule("age", .{
.root_source_file = b.path("age.zig"),
});
// Build the example executable
const example = b.addExecutable(.{
.name = "age-example",
.root_module = b.createModule(.{
.root_source_file = b.path("example.zig"),
.target = target,
.optimize = optimize,
}),
});
// Add the age module to the example
example.root_module.addImport("age", age_module);
// Link the Rust static library
// Assumes the library has been built with: cargo build --release
example.root_module.addLibraryPath(b.path("../target/release"));
example.root_module.linkSystemLibrary("age_ffi", .{ .needed = true });
// example.linkLibC();
// Install the example
b.installArtifact(example);
// Create run step for the example
const run_cmd = b.addRunArtifact(example);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the example");
run_step.dependOn(&run_cmd.step);
// Add a step to build the Rust library first
const cargo_build = b.addSystemCommand(&[_][]const u8{
"cargo",
"build",
"--release",
"--manifest-path",
"../Cargo.toml",
});
const cargo_step = b.step("cargo", "Build the Rust library");
cargo_step.dependOn(&cargo_build.step);
// Make the example depend on the cargo build
example.step.dependOn(&cargo_build.step);
// Add a clean step
const cargo_clean = b.addSystemCommand(&[_][]const u8{
"cargo",
"clean",
"--manifest-path",
"../Cargo.toml",
});
const clean_step = b.step("clean", "Clean build artifacts");
clean_step.dependOn(&cargo_clean.step);
// Add test step
const tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("test.zig"),
.target = target,
.optimize = optimize,
}),
});
tests.root_module.addImport("age", age_module);
tests.root_module.addLibraryPath(b.path("../target/release"));
tests.root_module.linkSystemLibrary("age_ffi", .{ .needed = true });
// tests.linkLibC();
tests.step.dependOn(&cargo_build.step);
const test_run = b.addRunArtifact(tests);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&test_run.step);
}

View File

@@ -0,0 +1,9 @@
.{
.name = .age,
.version = "0.1.0",
.fingerprint = 0xa13010b27f1528d3,
.minimum_zig_version = "0.14.0",
.paths = .{
"zig",
},
}

View File

@@ -0,0 +1,194 @@
//! Example usage of the age-ffi Zig bindings
//!
//! This file demonstrates various encryption/decryption operations using the age library.
const std = @import("std");
const age = @import("age.zig");
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
// Set up unbuffered stdout for Zig 0.15+ (simpler for examples)
// var stdout_writer = std.fs.File.stdout().writer(&.{});
var stdout_writer = std.Io.File.stdout().writer(io, &.{});
const stdout = &stdout_writer.interface;
try stdout.print("age-ffi Zig Bindings Example\n", .{});
try stdout.print("============================\n\n", .{});
// Print version information
try stdout.print("Library version: {s}\n", .{age.getVersion()});
try stdout.print("Age library version: {s}\n\n", .{age.getLibVersion()});
// Example 1: Generate a keypair
try stdout.print("Example 1: Generating a keypair\n", .{});
try stdout.print("--------------------------------\n", .{});
var keypair = try age.generateKeypair();
defer keypair.deinit();
try stdout.print("Public key: {s}\n", .{keypair.getPublicKey()});
try stdout.print("Private key: {s}\n\n", .{keypair.getPrivateKey()});
try stdout.flush();
// Example 2: Simple encryption and decryption
try stdout.print("Example 2: Simple encryption/decryption\n", .{});
try stdout.print("---------------------------------------\n", .{});
const plaintext = "Hello, World! This is a secret message.";
try stdout.print("Original: {s}\n", .{plaintext});
// Encrypt
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
defer encrypted.deinit();
try stdout.print("Encrypted: {} bytes\n", .{encrypted.buffer.len});
// Decrypt
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
defer decrypted.deinit();
try stdout.print("Decrypted: {s}\n\n", .{decrypted.toSlice()});
try stdout.flush();
// Example 3: ASCII armor
try stdout.print("Example 3: ASCII armor encryption\n", .{});
try stdout.print("----------------------------------\n", .{});
var armored = try age.encryptArmor("This message will be ASCII armored.", keypair.getPublicKey());
defer armored.deinit();
try stdout.print("Encrypted with ASCII armor: {} bytes\n", .{armored.buffer.len});
// Decrypt armored message
var decrypted_armored = try age.decrypt(armored.toSlice(), keypair.getPrivateKey());
defer decrypted_armored.deinit();
try stdout.print("Decrypted successfully: {s}\n\n", .{decrypted_armored.toSlice()});
try stdout.flush();
// Example 4: Passphrase-based encryption
try stdout.print("Example 4: Passphrase encryption\n", .{});
try stdout.print("---------------------------------\n", .{});
const passphrase = "super-secret-passphrase";
const secret_data = "Encrypted with a passphrase!";
// Encrypt without armor (armor with passphrase has decryption issues in upstream library)
var pass_encrypted = try age.encryptPassphrase(secret_data, passphrase, false);
defer pass_encrypted.deinit();
try stdout.print("Passphrase-encrypted: {} bytes\n", .{pass_encrypted.buffer.len});
var pass_decrypted = try age.decryptPassphrase(pass_encrypted.toSlice(), passphrase);
defer pass_decrypted.deinit();
try stdout.print("Decrypted: {s}\n\n", .{pass_decrypted.toSlice()});
try stdout.flush();
// Example 5: Multiple recipients
try stdout.print("Example 5: Multiple recipients\n", .{});
try stdout.print("-------------------------------\n", .{});
// Generate a second keypair
var keypair2 = try age.generateKeypair();
defer keypair2.deinit();
try stdout.print("Recipient 1: {s}\n", .{keypair.getPublicKey()});
try stdout.print("Recipient 2: {s}\n", .{keypair2.getPublicKey()});
// Create array of recipients
const recipients = [_][:0]const u8{
keypair.getPublicKey(),
keypair2.getPublicKey(),
};
const multi_plaintext = "This can be decrypted by either recipient!";
var multi_encrypted = try age.encryptMulti(multi_plaintext, &recipients, false);
defer multi_encrypted.deinit();
try stdout.print("Encrypted for both recipients ({} bytes)\n", .{multi_encrypted.buffer.len});
// Decrypt with first identity
var multi_decrypted1 = try age.decrypt(multi_encrypted.toSlice(), keypair.getPrivateKey());
defer multi_decrypted1.deinit();
try stdout.print("Decrypted with key 1: {s}\n", .{multi_decrypted1.toSlice()});
// Decrypt with second identity
var multi_decrypted2 = try age.decrypt(multi_encrypted.toSlice(), keypair2.getPrivateKey());
defer multi_decrypted2.deinit();
try stdout.print("Decrypted with key 2: {s}\n\n", .{multi_decrypted2.toSlice()});
try stdout.flush();
// Example 6: File operations
try stdout.print("Example 6: File encryption/decryption\n", .{});
try stdout.print("--------------------------------------\n", .{});
const file_data = "This will be written to an encrypted file.";
const encrypted_file = "/tmp/test.age";
// Encrypt to file (non-armored)
try age.encryptToFile(file_data, keypair.getPublicKey(), encrypted_file);
try stdout.print("Encrypted to file: {s}\n", .{encrypted_file});
// Decrypt from file
var file_decrypted = try age.decryptFileWithIdentity(encrypted_file, keypair.getPrivateKey());
defer file_decrypted.deinit();
try stdout.print("Decrypted from file: {s}\n\n", .{file_decrypted.toSlice()});
try stdout.flush();
// Example 7: Validation
try stdout.print("Example 7: Key validation\n", .{});
try stdout.print("--------------------------\n", .{});
const valid_recipient = keypair.getPublicKey();
const valid_identity = keypair.getPrivateKey();
const invalid_key = "not-a-valid-key";
try stdout.print("Is '{s}' a valid recipient? {}\n", .{ valid_recipient, age.isValidX25519Recipient(valid_recipient) });
try stdout.print("Is '{s}' a valid identity? {}\n", .{ valid_identity, age.isValidX25519Identity(valid_identity) });
try stdout.print("Is '{s}' a valid recipient? {}\n", .{ invalid_key, age.isValidX25519Recipient(invalid_key) });
const recipient_type = age.getRecipientType(valid_recipient);
try stdout.print("Recipient type: {s}\n\n", .{@tagName(recipient_type)});
try stdout.flush();
// Example 8: Deriving public key from private key
try stdout.print("Example 8: Derive public key\n", .{});
try stdout.print("-----------------------------\n", .{});
const derived_public = try age.derivePublicKey(gpa, keypair.getPrivateKey());
defer gpa.free(derived_public);
try stdout.print("Original public: {s}\n", .{keypair.getPublicKey()});
try stdout.print("Derived public: {s}\n", .{derived_public});
try stdout.print("Keys match: {}\n\n", .{std.mem.eql(u8, keypair.getPublicKey(), derived_public)});
try stdout.flush();
// Example 9: Error handling
try stdout.print("Example 9: Error handling\n", .{});
try stdout.print("-------------------------\n", .{});
// Try to decrypt with wrong key
if (age.decrypt(encrypted.toSlice(), keypair2.getPrivateKey())) |_| {
try stdout.print("Unexpected success!\n", .{});
} else |err| {
try stdout.print("Expected error: {s}\n", .{@errorName(err)});
}
// Try to use invalid passphrase
if (age.decryptPassphrase(pass_encrypted.toSlice(), "wrong-passphrase")) |_| {
try stdout.print("Unexpected success!\n", .{});
} else |err| {
try stdout.print("Expected error: {s}\n", .{@errorName(err)});
}
try stdout.print("\nAll examples completed successfully!\n", .{});
// Flush all output to ensure it's displayed
try stdout.flush();
}

View File

@@ -0,0 +1,317 @@
//! Test suite for age-ffi Zig bindings
const std = @import("std");
const age = @import("age.zig");
const testing = std.testing;
test "version information" {
const version = age.getVersion();
const lib_version = age.getLibVersion();
try testing.expect(version.len > 0);
try testing.expect(lib_version.len > 0);
std.debug.print("\nLibrary version: {s}\n", .{version});
std.debug.print("Age library version: {s}\n", .{lib_version});
}
test "generate keypair" {
var keypair = try age.generateKeypair();
defer keypair.deinit();
const public_key = keypair.getPublicKey();
const private_key = keypair.getPrivateKey();
try testing.expect(public_key.len > 0);
try testing.expect(private_key.len > 0);
try testing.expect(std.mem.startsWith(u8, public_key, "age1"));
try testing.expect(std.mem.startsWith(u8, private_key, "AGE-SECRET-KEY-1"));
std.debug.print("\nGenerated keypair:\n", .{});
std.debug.print(" Public: {s}\n", .{public_key});
std.debug.print(" Private: {s}\n", .{private_key});
}
test "derive public key from private" {
var keypair = try age.generateKeypair();
defer keypair.deinit();
const derived = try age.derivePublicKey(testing.allocator, keypair.getPrivateKey());
defer testing.allocator.free(derived);
try testing.expectEqualStrings(keypair.getPublicKey(), derived);
std.debug.print("\nDerived public key matches: ✓\n", .{});
}
test "simple encrypt and decrypt" {
var keypair = try age.generateKeypair();
defer keypair.deinit();
const plaintext = "Hello, World! This is a test message.";
// Encrypt
var encrypted = try age.encrypt(plaintext, keypair.getPublicKey());
defer encrypted.deinit();
try testing.expect(encrypted.buffer.len > 0);
std.debug.print("\nEncrypted {} bytes\n", .{encrypted.buffer.len});
// Decrypt
var decrypted = try age.decrypt(encrypted.toSlice(), keypair.getPrivateKey());
defer decrypted.deinit();
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
std.debug.print("Decrypted successfully: {s}\n", .{decrypted.toSlice()});
}
test "encrypt with armor" {
var keypair = try age.generateKeypair();
defer keypair.deinit();
const plaintext = "This message will be ASCII armored.";
std.debug.print("\nTesting ASCII armor encryption...\n", .{});
std.debug.print("Plaintext: {s}\n", .{plaintext});
std.debug.print("Recipient: {s}\n", .{keypair.getPublicKey()});
// Encrypt with armor
var encrypted = try age.encryptArmor(plaintext, keypair.getPublicKey());
defer encrypted.deinit();
std.debug.print("Buffer after encryption:\n", .{});
std.debug.print(" len: {}\n", .{encrypted.buffer.len});
std.debug.print(" capacity: {}\n", .{encrypted.buffer.capacity});
try testing.expect(encrypted.buffer.len > 0);
const ciphertext = encrypted.toSlice();
std.debug.print("Encrypted {} bytes\n", .{ciphertext.len});
// Check if it looks like ASCII armor
if (ciphertext.len > 0) {
const has_armor_header = std.mem.indexOf(u8, ciphertext, "-----BEGIN AGE ENCRYPTED FILE-----") != null;
std.debug.print("Has armor header: {}\n", .{has_armor_header});
if (ciphertext.len < 500) {
std.debug.print("Ciphertext:\n{s}\n", .{ciphertext});
}
}
// Decrypt
var decrypted = try age.decrypt(ciphertext, keypair.getPrivateKey());
defer decrypted.deinit();
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
std.debug.print("Decrypted successfully: {s}\n", .{decrypted.toSlice()});
}
test "passphrase encryption" {
const plaintext = "Secret message encrypted with passphrase";
const passphrase = "super-secret-password";
// Encrypt
var encrypted = try age.encryptPassphrase(plaintext, passphrase, false);
defer encrypted.deinit();
try testing.expect(encrypted.buffer.len > 0);
std.debug.print("\nPassphrase encrypted {} bytes\n", .{encrypted.buffer.len});
// Decrypt
var decrypted = try age.decryptPassphrase(encrypted.toSlice(), passphrase);
defer decrypted.deinit();
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
std.debug.print("Decrypted: {s}\n", .{decrypted.toSlice()});
}
test "passphrase encryption with armor (manual dearmor)" {
const plaintext = "Secret message with armor";
const passphrase = "test-password";
// Encrypt with armor
var encrypted = try age.encryptPassphrase(plaintext, passphrase, true);
defer encrypted.deinit();
try testing.expect(encrypted.buffer.len > 0);
std.debug.print("\nPassphrase encrypted with armor: {} bytes\n", .{encrypted.buffer.len});
const ciphertext = encrypted.toSlice();
const has_armor = std.mem.indexOf(u8, ciphertext, "-----BEGIN") != null;
try testing.expect(has_armor);
std.debug.print("Has ASCII armor: ✓\n", .{});
// For passphrase encryption, armored data must be dearmored before decryption
// (unlike x25519 encryption where age_decrypt auto-detects armor)
std.debug.print("Manually dearmoring before passphrase decryption...\n", .{});
var dearmored = try age.dearmor(ciphertext);
defer dearmored.deinit();
std.debug.print("Dearmored to {} bytes\n", .{dearmored.buffer.len});
// Now decrypt the binary data
var decrypted = try age.decryptPassphrase(dearmored.toSlice(), passphrase);
defer decrypted.deinit();
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
std.debug.print("Successfully decrypted armored passphrase data: ✓\n", .{});
}
test "passphrase encryption with armor (convenience function)" {
const plaintext = "Testing convenience function";
const passphrase = "convenient-pass";
// Encrypt with armor
var encrypted = try age.encryptPassphrase(plaintext, passphrase, true);
defer encrypted.deinit();
std.debug.print("\nTesting decryptPassphraseArmored convenience function...\n", .{});
// Use the convenience function that handles dearmoring automatically
var decrypted = try age.decryptPassphraseArmored(encrypted.toSlice(), passphrase);
defer decrypted.deinit();
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
std.debug.print("Convenience function works: ✓\n", .{});
}
test "multiple recipients" {
var keypair1 = try age.generateKeypair();
defer keypair1.deinit();
var keypair2 = try age.generateKeypair();
defer keypair2.deinit();
const plaintext = "Message for multiple recipients";
const recipients = [_][:0]const u8{
keypair1.getPublicKey(),
keypair2.getPublicKey(),
};
// Encrypt for both recipients
var encrypted = try age.encryptMulti(plaintext, &recipients, false);
defer encrypted.deinit();
try testing.expect(encrypted.buffer.len > 0);
std.debug.print("\nEncrypted for {} recipients: {} bytes\n", .{ recipients.len, encrypted.buffer.len });
// Decrypt with first key
var decrypted1 = try age.decrypt(encrypted.toSlice(), keypair1.getPrivateKey());
defer decrypted1.deinit();
try testing.expectEqualStrings(plaintext, decrypted1.toSlice());
std.debug.print("Decrypted with key 1: ✓\n", .{});
// Decrypt with second key
var decrypted2 = try age.decrypt(encrypted.toSlice(), keypair2.getPrivateKey());
defer decrypted2.deinit();
try testing.expectEqualStrings(plaintext, decrypted2.toSlice());
std.debug.print("Decrypted with key 2: ✓\n", .{});
}
test "validation functions" {
var keypair = try age.generateKeypair();
defer keypair.deinit();
// Valid keys
try testing.expect(age.isValidX25519Recipient(keypair.getPublicKey()));
try testing.expect(age.isValidX25519Identity(keypair.getPrivateKey()));
std.debug.print("\nValidation tests:\n", .{});
std.debug.print(" Valid recipient: ✓\n", .{});
std.debug.print(" Valid identity: ✓\n", .{});
// Invalid keys
try testing.expect(!age.isValidX25519Recipient("not-a-key"));
try testing.expect(!age.isValidX25519Identity("not-a-key"));
std.debug.print(" Invalid key detection: ✓\n", .{});
// Recipient type
const recip_type = age.getRecipientType(keypair.getPublicKey());
try testing.expectEqual(age.RecipientType.x25519, recip_type);
std.debug.print(" Recipient type: {s}\n", .{@tagName(recip_type)});
}
test "error handling - wrong key" {
var keypair1 = try age.generateKeypair();
defer keypair1.deinit();
var keypair2 = try age.generateKeypair();
defer keypair2.deinit();
const plaintext = "Encrypted for keypair1";
var encrypted = try age.encrypt(plaintext, keypair1.getPublicKey());
defer encrypted.deinit();
// Try to decrypt with wrong key
const result = age.decrypt(encrypted.toSlice(), keypair2.getPrivateKey());
try testing.expectError(age.AgeError.DecryptionFailed, result);
std.debug.print("\nWrong key error: ✓\n", .{});
}
test "error handling - invalid recipient" {
const plaintext = "Test message";
const invalid_recipient = "not-a-valid-recipient";
const result = age.encrypt(plaintext, invalid_recipient);
try testing.expectError(age.AgeError.InvalidRecipient, result);
std.debug.print("\nInvalid recipient error: ✓\n", .{});
}
test "error handling - invalid passphrase" {
const plaintext = "Secret";
const correct_pass = "correct";
const wrong_pass = "wrong";
var encrypted = try age.encryptPassphrase(plaintext, correct_pass, false);
defer encrypted.deinit();
const result = age.decryptPassphrase(encrypted.toSlice(), wrong_pass);
// Note: The underlying age library returns DecryptionFailed for wrong passphrase
// rather than a specific InvalidPassphrase error
try testing.expectError(age.AgeError.DecryptionFailed, result);
std.debug.print("\nInvalid passphrase error: ✓\n", .{});
}
test "armor and dearmor operations" {
const data = "Some binary data to armor";
// Armor the data
var armored = try age.armor(data);
defer armored.deinit();
try testing.expect(armored.buffer.len > 0);
std.debug.print("\nArmored {} bytes -> {} bytes\n", .{ data.len, armored.buffer.len });
const armored_data = armored.toSlice();
const has_header = std.mem.indexOf(u8, armored_data, "-----BEGIN") != null;
try testing.expect(has_header);
// Dearmor it
var dearmored = try age.dearmor(armored_data);
defer dearmored.deinit();
try testing.expectEqualStrings(data, dearmored.toSlice());
std.debug.print("Dearmored successfully: ✓\n", .{});
}
test "file operations" {
const tmp_file = "/tmp/age_test_encrypted.age";
const plaintext = "File encryption test data";
var keypair = try age.generateKeypair();
defer keypair.deinit();
// Encrypt to file
try age.encryptToFile(plaintext, keypair.getPublicKey(), tmp_file);
std.debug.print("\nEncrypted to file: {s}\n", .{tmp_file});
// Decrypt from file
var decrypted = try age.decryptFileWithIdentity(tmp_file, keypair.getPrivateKey());
defer decrypted.deinit();
try testing.expectEqualStrings(plaintext, decrypted.toSlice());
std.debug.print("Decrypted from file: ✓\n", .{});
// Clean up
std.fs.cwd().deleteFile(tmp_file) catch {};
}

View File

@@ -0,0 +1 @@
/zig-cache

4
zig-vendor/zig-sqlite/.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
c/sqlite3.c linguist-vendored
c/sqlite3.h linguist-vendored
zig.mod linguist-vendored
*.zig text=auto eol=lf

View File

@@ -0,0 +1,44 @@
name: Bug report
description: Create a bug report
labels:
- bug
body:
- type: markdown
attributes:
value: |
# A bug means something doesn't work as expected
Remember to include as much detail as possible.
- type: input
id: commit
attributes:
label: zig-sqlite commit
description: "The git commit of zig-sqlite"
validations:
required: true
- type: input
id: zig_version
attributes:
label: Zig version
description: "The output of `zig version`"
placeholder: "0.11.0-dev.3335+3085e2af4"
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
description: How can someone reproduce the problem you encountered ? Include a self-contained reproducer if possible
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What did you expect to happen?
validations:
required: true

View File

@@ -0,0 +1,6 @@
# Description
Please describe the changes you want to make and why. Please also provide an explanation of the implementation.
As a rule of thumb, give as much detail as you would want to see if you were to review this PR.
If this PR closes an issue, please reference it with something like "Closes #issue".

View File

@@ -0,0 +1,62 @@
name: CI
on:
create:
push:
branches: master
paths:
- '**.zig'
pull_request:
schedule:
- cron: "0 13 * * *"
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: master
- run: zig fmt --check *.zig
test-in-memory:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig
uses: mlugg/setup-zig@v2
with:
version: master
- name: Install qemu
if: ${{ matrix.os == 'ubuntu-24.04' }}
run: |
sudo apt-get update -y && sudo apt-get install -y qemu-user-binfmt
- name: Restore cache
uses: actions/cache@v4
with:
path: |
zig-cache
~/.cache/zig
key: ${{ runner.os }}-${{ matrix.os }}-zig-${{ github.sha }}
restore-keys: ${{ runner.os }}-${{ matrix.os }}-zig-
- name: Run Tests in memory
if: ${{ matrix.os == 'ubuntu-24.04' }}
run: zig build test -Dci=true -Din_memory=true --summary all -fqemu -fwine
- name: Run Tests in memory
if: ${{ matrix.os != 'ubuntu-24.04' }}
run: zig build test -Dci=true -Din_memory=true --summary all

8
zig-vendor/zig-sqlite/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/build_runner.zig
/.zig-cache
zig-out
.zigmod
deps.zig
core.*
/qemu*.core
/fuzz/outputs

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Vincent Rischmann <vincent@rischmann.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,652 @@
# zig-sqlite
This package is a thin wrapper around [sqlite](https://sqlite.org/index.html)'s C API.
_Maintainer note_: I'm currently on a break working with Zig and don't intend to work on new features for zig-sqlite.
I will keep it updated for the latest Zig versions because that doesn't take too much of my time.
# Status
While the core functionality works right now, the API is still subject to changes.
If you use this library, expect to have to make changes when you update the code.
# Zig release support
`zig-sqlite` follows Zig's release structure:
- [master](https://github.com/vrischmann/zig-sqlite) tracks Zig master
- [zig-0.15.1](https://github.com/vrischmann/zig-sqlite/tree/zig-0.15.1) tracks Zig 0.15.1
The plan is to support releases once Zig 1.0 is released but this can still change.
# Table of contents
<!--toc:start-->
- [zig-sqlite](#zig-sqlite)
- [Status](#status)
- [Zig release support](#zig-release-support)
- [Table of contents](#table-of-contents)
- [Requirements](#requirements)
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Demo](#demo)
- [Initialization](#initialization)
- [Preparing a statement](#preparing-a-statement)
- [Common use](#common-use)
- [Diagnostics](#diagnostics)
- [Executing a statement](#executing-a-statement)
- [Reuse a statement](#reuse-a-statement)
- [Reading data in one go](#reading-data-in-one-go)
- [Type parameter](#type-parameter)
- [`Statement.one`](#statementone)
- [`Statement.all` and `Statement.oneAlloc`](#statementall-and-statementonealloc)
- [Iterating](#iterating)
- [`Iterator.next`](#iteratornext)
- [`Iterator.nextAlloc`](#iteratornextalloc)
- [Bind parameters and resultset rows](#bind-parameters-and-resultset-rows)
- [Custom type binding and reading](#custom-type-binding-and-reading)
- [Note about complex allocations](#note-about-complex-allocations)
- [Comptime checks](#comptime-checks)
- [Check the number of bind parameters.](#check-the-number-of-bind-parameters)
- [Assign types to bind markers and check them.](#assign-types-to-bind-markers-and-check-them)
- [User defined SQL functions](#user-defined-sql-functions)
- [Scalar functions](#scalar-functions)
- [Aggregate functions](#aggregate-functions)
<!--toc:end-->
# Requirements
[Zig master](https://ziglang.org/download/) is the only required dependency.
For sqlite, you have options depending on your target:
* On Windows the only supported way at the moment to build `zig-sqlite` is with the bundled sqlite source code file.
* On Linux we have two options:
* use the system and development package for sqlite (`libsqlite3-dev` for Debian and derivatives, `sqlite3-devel` for Fedora)
* use the bundled sqlite source code file.
# Features
* Preparing, executing statements
* comptime checked bind parameters
* user defined SQL functions
# Installation
Use the following `zig fetch` command:
```
zig fetch --save git+https://github.com/vrischmann/zig-sqlite
```
Now in your `build.zig` you can access the module like this:
```zig
const sqlite = b.dependency("sqlite", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("sqlite", sqlite.module("sqlite"));
```
# Usage
## Demo
See https://github.com/vrischmann/zig-sqlite-demo for a quick demo.
## Initialization
Import `zig-sqlite` like this:
```zig
const sqlite = @import("sqlite");
```
You must create and initialize an instance of `sqlite.Db`:
```zig
var db = try sqlite.Db.init(.{
.mode = sqlite.Db.Mode{ .File = "/home/vincent/mydata.db" },
.open_flags = .{
.write = true,
.create = true,
},
.threading_mode = .MultiThread,
});
```
The `init` method takes a `InitOptions` struct which will be used to configure sqlite.
Only the `mode` field is mandatory, the other fields have sane default values.
## Preparing a statement
### Common use
sqlite works exclusively by using prepared statements. The wrapper type is `sqlite.Statement`. Here is how you get one:
```zig
try db.exec("CREATE TABLE IF NOT EXISTS employees(id integer primary key, name text, age integer, salary integer)", .{}, .{});
const query =
\\SELECT id, name, age, salary FROM employees WHERE age > ? AND age < ?
;
var stmt = try db.prepare(query);
defer stmt.deinit();
```
The `Db.prepare` method takes a `comptime` query string.
### Diagnostics
If you want failure diagnostics you can use `prepareWithDiags` like this:
```zig
var diags = sqlite.Diagnostics{};
var stmt = db.prepareWithDiags(query, .{ .diags = &diags }) catch |err| {
std.log.err("unable to prepare statement, got error {}. diagnostics: {s}", .{ err, diags });
return err;
};
defer stmt.deinit();
```
## Executing a statement
For queries which do not return data (`INSERT`, `UPDATE`) you can use the `exec` method:
```zig
const query =
\\INSERT INTO employees(name, age, salary) VALUES(?, ?, ?)
;
var stmt = try db.prepare(query);
defer stmt.deinit();
try stmt.exec(.{}, .{
.name = "José",
.age = 40,
.salary = 20000,
});
```
See the section "Bind parameters and resultset rows" for more information on the types mapping rules.
## Reuse a statement
You can reuse a statement by resetting it like this:
```zig
const query =
\\UPDATE employees SET salary = ? WHERE id = ?
;
var stmt = try db.prepare(query);
defer stmt.deinit();
var id: usize = 0;
while (id < 20) : (id += 1) {
stmt.reset();
try stmt.exec(.{}, .{
.salary = 2000,
.id = id,
});
}
```
## Reading data in one go
For queries which return data you have multiple options:
* `Statement.all` which takes an allocator and can allocate memory.
* `Statement.one` which does not take an allocator and cannot allocate memory (aside from what sqlite allocates itself).
* `Statement.oneAlloc` which takes an allocator and can allocate memory.
### Type parameter
All these methods take a type as first parameter.
The type represents a "row", it can be:
* a struct where each field maps to the corresponding column in the resultset (so field 0 must map to column 1 and so on).
* a single type, in that case the resultset must only return one column.
The type can be a pointer but only when using the methods taking an allocator.
Not all types are allowed, see the section "Bind parameters and resultset rows" for more information on the types mapping rules.
### `Statement.one`
Using `one`:
```zig
const query =
\\SELECT name, age FROM employees WHERE id = ?
;
var stmt = try db.prepare(query);
defer stmt.deinit();
const row = try stmt.one(
struct {
name: [128:0]u8,
age: usize,
},
.{},
.{ .id = 20 },
);
if (row) |r| {
const name_ptr: [*:0]const u8 = &r.name;
std.log.debug("name: {s}, age: {}", .{ std.mem.span(name_ptr), r.age });
}
```
Notice that to read text we need to use a 0-terminated array; if the `name` column is bigger than 127 bytes the call to `one` will fail.
If the length of the data is variable then the sentinel is mandatory: without one there would be no way to know where the data ends in the array.
However if the length is fixed, you can read into a non 0-terminated array, for example:
```zig
const query =
\\SELECT id FROM employees WHERE name = ?
;
var stmt = try db.prepare(query);
defer stmt.deinit();
const row = try stmt.one(
[16]u8,
.{},
.{ .name = "Vincent" },
);
if (row) |id| {
std.log.debug("id: {s}", .{std.fmt.fmtSliceHexLower(&id)});
}
```
If the column data doesn't have the correct length a `error.ArraySizeMismatch` will be returned.
The convenience function `sqlite.Db.one` works exactly the same way:
```zig
const query =
\\SELECT age FROM employees WHERE id = ?
;
const row = try db.one(usize, query, .{}, .{ .id = 20 });
if (row) |age| {
std.log.debug("age: {}", .{age});
}
```
### `Statement.all` and `Statement.oneAlloc`
Using `all`:
```zig
const query =
\\SELECT name FROM employees WHERE age > ? AND age < ?
;
var stmt = try db.prepare(query);
defer stmt.deinit();
const allocator = std.heap.page_allocator; // Use a suitable allocator
const names = try stmt.all([]const u8, allocator, .{}, .{
.age1 = 20,
.age2 = 40,
});
for (names) |name| {
std.log.debug("name: {s}", .{ name });
}
```
Using `oneAlloc`:
```zig
const query =
\\SELECT name FROM employees WHERE id = ?
;
var stmt = try db.prepare(query);
defer stmt.deinit();
const allocator = std.heap.page_allocator; // Use a suitable allocator
const row = try stmt.oneAlloc([]const u8, allocator, .{}, .{
.id = 200,
});
if (row) |name| {
std.log.debug("name: {s}", .{name});
}
```
## Iterating
Another way to get the data returned by a query is to use the `sqlite.Iterator` type.
You can only get one by calling the `iterator` method on a statement.
The `iterator` method takes a type which is the same as with `all`, `one` or `oneAlloc`: every row retrieved by calling `next` or `nextAlloc` will have this type.
Iterating is done by calling the `next` or `nextAlloc` method on an iterator. Just like before, `next` cannot allocate memory while `nextAlloc` can allocate memory.
`next` or `nextAlloc` will either return an optional value or an error; you should keep iterating until `null` is returned.
### `Iterator.next`
```zig
var stmt = try db.prepare("SELECT age FROM employees WHERE age < ?");
defer stmt.deinit();
var iter = try stmt.iterator(usize, .{
.age = 20,
});
while (try iter.next(.{})) |age| {
std.debug.print("age: {}\n", .{age});
}
```
### `Iterator.nextAlloc`
```zig
var stmt = try db.prepare("SELECT name FROM employees WHERE age < ?");
defer stmt.deinit();
var iter = try stmt.iterator([]const u8, .{
.age = 20,
});
const allocator = std.heap.page_allocator; // Use a suitable allocator
while (true) {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const name = (try iter.nextAlloc(arena.allocator(), .{})) orelse break;
std.debug.print("name: {s}\n", .{name});
}
```
## Bind parameters and resultset rows
Since sqlite doesn't have many [types](https://www.sqlite.org/datatype3.html) only a small number of Zig types are allowed in binding parameters and in resultset mapping types.
Here are the rules for bind parameters:
* any Zig `Int` or `ComptimeInt` is treated as a `INTEGER`.
* any Zig `Float` or `ComptimeFloat` is treated as a `REAL`.
* `[]const u8`, `[]u8` is treated as a `TEXT`.
* the custom `sqlite.Blob` type is treated as a `BLOB`.
* the custom `sqlite.Text` type is treated as a `TEXT`.
* the `null` value is treated as a `NULL`.
* non-null optionals are treated like a regular value, null optionals are treated as a `NULL`.
Here are the rules for resultset rows:
* `INTEGER` can be read into any Zig `Int` provided the data fits.
* `REAL` can be read into any Zig `Float` provided the data fits.
* `TEXT` can be read into a `[]const u8` or `[]u8`.
* `TEXT` can be read into any array of `u8` with a sentinel provided the data fits.
* `BLOB` follows the same rules as `TEXT`.
* `NULL` can be read into any optional.
Note that arrays must have a sentinel because we need a way to communicate where the data actually stops in the array, so for example use `[200:0]u8` for a `TEXT` field.
## Custom type binding and reading
Sometimes the default field binding or reading logic is not what you want, for example if you want to store an enum using its tag name instead of its integer value or
if you want to store a byte slice as an hex string.
To accomplish this you must first define a wrapper struct for your type. For example if your type is a `[4]u8` and you want to treat it as an integer:
```zig
pub const MyArray = struct {
data: [4]u8,
pub const BaseType = u32;
pub fn bindField(self: MyArray, _: std.mem.Allocator) !BaseType {
return std.mem.readIntNative(BaseType, &self.data);
}
pub fn readField(_: std.mem.Allocator, value: BaseType) !MyArray {
var arr: MyArray = undefined;
std.mem.writeIntNative(BaseType, &arr.data, value);
return arr;
}
};
```
Now when you bind a value of type `MyArray` the value returned by `bindField` will be used for binding instead.
Same for reading, when you select _into_ a `MyArray` row or field the value returned by `readField` will be used instead.
_NOTE_: when you _do_ allocate in `bindField` or `readField` make sure to pass a `std.heap.ArenaAllocator`-based allocator.
The binding or reading code does not keep tracking of allocations made in custom types so it can't free the allocated data itself; it's therefore required
to use an arena to prevent memory leaks.
## Note about complex allocations
Depending on your queries and types there can be a lot of allocations required. Take the following example:
```zig
const User = struct {
id: usize,
first_name: []const u8,
last_name: []const u8,
data: []const u8,
};
fn fetchUsers(allocator: std.mem.Allocator, db: *sqlite.Db) ![]User {
var stmt = try db.prepare("SELECT id FROM user WHERE id > $id");
defer stmt.deinit();
return stmt.all(User, allocator, .{}, .{ .id = 20 });
}
```
This will do multiple allocations:
* one for each id field in the `User` type
* one for the resulting slice
To facilitate memory handling, consider using an arena allocator like this:
```zig
const allocator = std.heap.page_allocator; // Use a suitable allocator
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const users = try fetchUsers(arena.allocator(), db);
_ = users;
```
This is especially recommended if you use custom types that allocate memory since, as noted above, it's necessary to prevent memory leaks.
# Comptime checks
Prepared statements contain _comptime_ metadata which is used to validate every call to `exec`, `one` and `all` _at compile time_.
## Check the number of bind parameters.
The first check makes sure you provide the same number of bind parameters as there are bind markers in the query string.
Take the following code:
```zig
var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?");
defer stmt.deinit();
const allocator = std.heap.page_allocator; // Use a suitable allocator
const rows = try stmt.all(usize, allocator, .{}, .{
.age_1 = 10,
.age_2 = 20,
});
_ = rows;
```
It fails with this compilation error:
```
/home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:738:17: error: number of bind markers not equal to number of fields
@compileError("number of bind markers not equal to number of fields");
^
/home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:817:22: note: called from here
self.bind(values);
^
/home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:905:41: note: called from here
var iter = try self.iterator(Type, values);
^
./src/main.zig:19:30: note: called from here
const rows = try stmt.all(usize, allocator, .{}, .{
^
./src/main.zig:5:29: note: called from here
pub fn main() anyerror!void {
```
## Assign types to bind markers and check them.
The second (and more interesting) check makes sure you provide appropriately typed values as bind parameters.
This check is not automatic since with a standard SQL query we have no way to know the types of the bind parameters, to use it you must provide theses types in the SQL query with a custom syntax.
For example, take the same code as above but now we also bind the last parameter:
```zig
var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?");
defer stmt.deinit();
const allocator = std.heap.page_allocator; // Use a suitable allocator
const rows = try stmt.all(usize, allocator, .{}, .{
.age_1 = 10,
.age_2 = 20,
.weight = false,
});
_ = rows;
```
This compiles correctly even if the `weight` field in our `user` table is of the type `INTEGER`.
We can make sure the bind parameters have the right type if we rewrite the query like this:
```zig
var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?{usize}");
defer stmt.deinit();
const allocator = std.heap.page_allocator; // Use a suitable allocator
const rows = try stmt.all(usize, allocator, .{}, .{
.age_1 = 10,
.age_2 = 20,
.weight = false,
});
_ = rows;
```
Now this fails to compile:
```
/home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:745:25: error: value type bool is not the bind marker type usize
@compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(typ));
^
/home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:817:22: note: called from here
self.bind(values);
^
/home/vincent/dev/perso/libs/zig-sqlite/sqlite.zig:905:41: note: called from here
var iter = try self.iterator(Type, values);
^
./src/main.zig:19:30: note: called from here
const rows = try stmt.all(usize, allocator, .{}, .{
^
./src/main.zig:5:29: note: called from here
pub fn main() anyerror!void {
```
The syntax is straightforward: a bind marker `?` followed by `{`, a Zig type name and finally `}`.
There are a limited number of types allowed currently:
* all [integer](https://ziglang.org/documentation/master/#Primitive-Types) types.
* all [arbitrary bit-width integer](https://ziglang.org/documentation/master/#Primitive-Types) types.
* all [float](https://ziglang.org/documentation/master/#Primitive-Types) types.
* bool.
* strings with `[]const u8` or `[]u8`.
* strings with `sqlite.Text`.
* blobs with `sqlite.Blob`.
It is probably possible to support arbitrary types if they can be marshaled to an SQLite type. This is something to investigate.
**NOTE**: this is done at compile time and is quite CPU intensive, therefore it's possible you'll have to play with [@setEvalBranchQuota](https://ziglang.org/documentation/master/#setEvalBranchQuota) to make it compile.
To finish our example, passing the proper type allows it compile:
```zig
var stmt = try db.prepare("SELECT id FROM user WHERE age > ? AND age < ? AND weight > ?{usize}");
defer stmt.deinit();
const allocator = std.heap.page_allocator; // Use a suitable allocator
const rows = try stmt.all(usize, allocator, .{}, .{
.age_1 = 10,
.age_2 = 20,
.weight = @as(usize, 200),
});
_ = rows;
```
# User defined SQL functions
sqlite supports [user-defined SQL functions](https://www.sqlite.org/c3ref/create_function.html) which come in two types:
* scalar functions
* aggregate functions
In both cases the arguments are [sqlite3\_values](https://www.sqlite.org/c3ref/value_blob.html) and are converted to Zig values using the following rules:
* `TEXT` values can be either `sqlite.Text` or `[]const u8`
* `BLOB` values can be either `sqlite.Blob` or `[]const u8`
* `INTEGER` values can be any Zig integer
* `REAL` values can be any Zig float
## Scalar functions
You can define a scalar function using `db.createScalarFunction`:
```zig
try db.createScalarFunction(
"blake3",
struct {
fn run(input: []const u8) [std.crypto.hash.Blake3.digest_length]u8 {
var hash: [std.crypto.hash.Blake3.digest_length]u8 = undefined;
std.crypto.hash.Blake3.hash(input, &hash, .{});
return hash;
}
}.run,
.{},
);
const hash = try db.one([std.crypto.hash.Blake3.digest_length]u8, "SELECT blake3('hello')", .{}, .{});
```
Each input arguments in the function call in the statement is passed on to the registered `run` function.
## Aggregate functions
You can define an aggregate function using `db.createAggregateFunction`:
```zig
const MyContext = struct {
sum: u32,
};
var my_ctx = MyContext{ .sum = 0 };
try db.createAggregateFunction(
"mySum",
&my_ctx,
struct {
fn step(fctx: sqlite.FunctionContext, input: u32) void {
var ctx = fctx.userContext(*MyContext) orelse return;
ctx.sum += input;
}
}.step,
struct {
fn finalize(fctx: sqlite.FunctionContext) u32 {
const ctx = fctx.userContext(*MyContext) orelse return 0;
return ctx.sum;
}
}.finalize,
.{},
);
const result = try db.one(usize, "SELECT mySum(nb) FROM foobar", .{}, .{});
```
Each input arguments in the function call in the statement is passed on to the registered `step` function.
The `finalize` function is called once at the end.
The context (2nd argument of `createAggregateFunction`) can be whatever you want; both the `step` and `finalize` functions must
have their first argument of the same type as the context.

View File

@@ -0,0 +1,382 @@
const std = @import("std");
const debug = std.debug;
const heap = std.heap;
const mem = std.mem;
const ResolvedTarget = std.Build.ResolvedTarget;
const Query = std.Target.Query;
const builtin = @import("builtin");
const Preprocessor = @import("build/Preprocessor.zig");
fn getTarget(original_target: ResolvedTarget) ResolvedTarget {
var tmp = original_target;
if (tmp.result.isGnuLibC()) {
const min_glibc_version = std.SemanticVersion{
.major = 2,
.minor = 28,
.patch = 0,
};
const ver = tmp.result.os.version_range.linux.glibc;
if (ver.order(min_glibc_version) == .lt) {
std.debug.panic("sqlite requires glibc version >= 2.28", .{});
}
}
return tmp;
}
const TestTarget = struct {
query: Query,
single_threaded: bool = false,
};
const ci_targets = switch (builtin.target.cpu.arch) {
.x86_64 => switch (builtin.target.os.tag) {
.linux => [_]TestTarget{
TestTarget{ .query = .{ .cpu_arch = .x86_64, .abi = .musl } },
TestTarget{ .query = .{ .cpu_arch = .x86, .abi = .musl } },
TestTarget{ .query = .{ .cpu_arch = .aarch64, .abi = .musl } },
},
.windows => [_]TestTarget{
TestTarget{ .query = .{ .cpu_arch = .x86_64, .abi = .gnu } },
// Disabled due to https://github.com/ziglang/zig/issues/20047
// TestTarget{ .query = .{ .cpu_arch = .x86, .abi = .gnu } },
},
.macos => [_]TestTarget{
TestTarget{ .query = .{ .cpu_arch = .x86_64 } },
},
else => [_]TestTarget{},
},
else => [_]TestTarget{},
};
const all_test_targets = switch (builtin.target.cpu.arch) {
.x86_64 => switch (builtin.target.os.tag) {
.linux => [_]TestTarget{
TestTarget{ .query = .{} },
TestTarget{ .query = .{ .cpu_arch = .x86_64, .abi = .musl } },
TestTarget{ .query = .{ .cpu_arch = .x86, .abi = .musl } },
TestTarget{ .query = .{ .cpu_arch = .aarch64, .abi = .musl } },
TestTarget{ .query = .{ .cpu_arch = .riscv64, .abi = .musl } },
// Disabled because it fails for some unknown reason
// TestTarget{ .query = .{ .cpu_arch = .mips, .abi = .musl } },
TestTarget{ .query = .{ .cpu_arch = .x86_64, .os_tag = .windows } },
// Disabled due to https://github.com/ziglang/zig/issues/20047
// TestTarget{ .query = .{ .cpu_arch = .x86, .os_tag = .windows } },
TestTarget{ .query = .{ .cpu_arch = .x86_64, .os_tag = .macos } },
TestTarget{ .query = .{ .cpu_arch = .aarch64, .os_tag = .macos } },
},
.windows => [_]TestTarget{
TestTarget{ .query = .{ .cpu_arch = .x86_64, .abi = .gnu } },
// Disabled due to https://github.com/ziglang/zig/issues/20047
// TestTarget{ .query = .{ .cpu_arch = .x86, .abi = .gnu } },
},
.freebsd => [_]TestTarget{
TestTarget{ .query = .{} },
TestTarget{ .query = .{ .cpu_arch = .x86_64 } },
},
.macos => [_]TestTarget{
TestTarget{ .query = .{ .cpu_arch = .x86_64 } },
},
else => [_]TestTarget{
TestTarget{ .query = .{} },
},
},
.aarch64 => switch (builtin.target.os.tag) {
.linux, .windows, .freebsd, .macos => [_]TestTarget{
TestTarget{ .query = .{} },
},
else => [_]TestTarget{
TestTarget{ .query = .{} },
},
},
else => [_]TestTarget{
TestTarget{ .query = .{} },
},
};
fn computeTestTargets(isNative: bool, ci: ?bool) ?[]const TestTarget {
if (ci != null and ci.?) return &ci_targets;
if (isNative) {
// If the target is native we assume the user didn't change it with -Dtarget and run all test targets.
return &all_test_targets;
}
// Otherwise we run a single test target.
return null;
}
// This creates a SQLite static library from the SQLite dependency code.
fn makeSQLiteLib(b: *std.Build, dep: *std.Build.Dependency, c_flags: []const []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, sqlite_c: enum { with, without }) *std.Build.Step.Compile {
const mod = b.addModule("lib-sqlite", .{
.target = target,
.optimize = optimize,
.link_libc = true,
});
const lib = b.addLibrary(.{
.name = "sqlite",
.linkage = .static,
.root_module = mod,
});
mod.addIncludePath(dep.path("."));
mod.addIncludePath(b.path("c"));
if (sqlite_c == .with) {
mod.addCSourceFile(.{
.file = dep.path("sqlite3.c"),
.flags = c_flags,
});
}
mod.addCSourceFile(.{
.file = b.path("c/workaround.c"),
.flags = c_flags,
});
return lib;
}
pub fn build(b: *std.Build) !void {
const in_memory = b.option(bool, "in_memory", "Should the tests run with sqlite in memory (default true)") orelse true;
const dbfile = b.option([]const u8, "dbfile", "Always use this database file instead of a temporary one");
const ci = b.option(bool, "ci", "Build and test in the CI on GitHub");
const query = b.standardTargetOptionsQueryOnly(.{});
const target = b.resolveTargetQuery(query);
const optimize = b.standardOptimizeOption(.{});
// Upstream dependency
const sqlite_dep = b.dependency("sqlite", .{
.target = target,
.optimize = optimize,
});
// Define C flags to use
var flags: std.ArrayList([]const u8) = .empty;
defer flags.deinit(b.allocator);
try flags.append(b.allocator, "-std=c99");
inline for (std.meta.fields(EnableOptions)) |field| {
const opt = b.option(bool, field.name, "Enable " ++ field.name) orelse field.defaultValue().?;
if (opt) {
var buf: [field.name.len]u8 = undefined;
const name = std.ascii.upperString(&buf, field.name);
const flag = try std.fmt.allocPrint(b.allocator, "-DSQLITE_ENABLE_{s}", .{name});
try flags.append(b.allocator, flag);
}
}
const c_flags = flags.items;
//
// Main library and module
//
// const sqlite_lib, const sqlite_mod = blk: {
const sqlite_lib, _ = blk: {
const lib = makeSQLiteLib(b, sqlite_dep, c_flags, target, optimize, .with);
const mod = b.addModule("sqlite", .{
.root_source_file = b.path("sqlite.zig"),
.link_libc = true,
});
mod.addIncludePath(b.path("c"));
mod.addIncludePath(sqlite_dep.path("."));
mod.linkLibrary(lib);
break :blk .{ lib, mod };
};
b.installArtifact(sqlite_lib);
// const sqliteext_mod = blk: {
_ = blk: {
const lib = makeSQLiteLib(b, sqlite_dep, c_flags, target, optimize, .without);
const mod = b.addModule("sqliteext", .{
.root_source_file = b.path("sqlite.zig"),
.link_libc = true,
});
mod.addIncludePath(b.path("c"));
mod.linkLibrary(lib);
break :blk mod;
};
//
// Tests
//
const test_targets = computeTestTargets(query.isNative(), ci) orelse &[_]TestTarget{.{
.query = query,
}};
const test_step = b.step("test", "Run library tests");
// By default the tests will only be execute for native test targets, however they will be compiled
// for _all_ targets defined in `test_targets`.
//
// If you want to execute tests for other targets you can pass -fqemu, -fdarling, -fwine, -frosetta.
for (test_targets) |test_target| {
const cross_target = getTarget(b.resolveTargetQuery(test_target.query));
const single_threaded_txt = if (test_target.single_threaded) "single" else "multi";
const test_name = b.fmt("{s}-{s}-{s}", .{
try cross_target.result.zigTriple(b.allocator),
@tagName(optimize),
single_threaded_txt,
});
const test_sqlite_lib = makeSQLiteLib(b, sqlite_dep, c_flags, cross_target, optimize, .with);
const mod = b.addModule(test_name, .{
.target = cross_target,
.optimize = optimize,
.root_source_file = b.path("sqlite.zig"),
.single_threaded = test_target.single_threaded,
});
const tests = b.addTest(.{
.name = test_name,
.root_module = mod,
});
tests.root_module.addIncludePath(b.path("c"));
tests.root_module.addIncludePath(sqlite_dep.path("."));
tests.root_module.linkLibrary(test_sqlite_lib);
const tests_options = b.addOptions();
tests.root_module.addImport("build_options", tests_options.createModule());
tests_options.addOption(bool, "in_memory", in_memory);
tests_options.addOption(?[]const u8, "dbfile", dbfile);
const run_tests = b.addRunArtifact(tests);
test_step.dependOn(&run_tests.step);
}
// This builds an example shared library with the extension and a binary that tests it.
//\ const zigcrypto_install_artifact = addZigcrypto(b, sqliteext_mod, target, optimize);
//\ test_step.dependOn(&zigcrypto_install_artifact.step);
//\ const zigcrypto_test_run = addZigcryptoTestRun(b, sqlite_mod, target, optimize);
//\ zigcrypto_test_run.step.dependOn(&zigcrypto_install_artifact.step);
//\ test_step.dependOn(&zigcrypto_test_run.step);
//
// Tools
//
addPreprocessStep(b, sqlite_dep);
}
fn addPreprocessStep(b: *std.Build, sqlite_dep: *std.Build.Dependency) void {
var wf = b.addWriteFiles();
// Preprocessing step
const preprocess = PreprocessStep.create(b, .{
.source = sqlite_dep.path("."),
.target = wf.getDirectory(),
});
preprocess.step.dependOn(&wf.step);
const w = b.addUpdateSourceFiles();
w.addCopyFileToSource(preprocess.target.join(b.allocator, "loadable-ext-sqlite3.h") catch @panic("OOM"), "c/loadable-ext-sqlite3.h");
w.addCopyFileToSource(preprocess.target.join(b.allocator, "loadable-ext-sqlite3ext.h") catch @panic("OOM"), "c/loadable-ext-sqlite3ext.h");
w.step.dependOn(&preprocess.step);
const preprocess_headers = b.step("preprocess-headers", "Preprocess the headers for the loadable extensions");
preprocess_headers.dependOn(&w.step);
}
fn addZigcrypto(b: *std.Build, sqlite_mod: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.InstallArtifact {
const mod = b.addModule("zigcryto", .{
.root_source_file = b.path("examples/zigcrypto.zig"),
.target = getTarget(target),
.optimize = optimize,
});
const exe = b.addLibrary(.{
.name = "zigcrypto",
.root_module = mod,
.version = null,
.linkage = .dynamic,
});
exe.root_module.addImport("sqlite", sqlite_mod);
const install_artifact = b.addInstallArtifact(exe, .{});
install_artifact.step.dependOn(&exe.step);
return install_artifact;
}
fn addZigcryptoTestRun(b: *std.Build, sqlite_mod: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.Run {
const mod = b.addModule("zigcryto-test", .{
.root_source_file = b.path("examples/zigcrypto_test.zig"),
.target = getTarget(target),
.optimize = optimize,
});
const zigcrypto_test = b.addExecutable(.{
.name = "zigcrypto-test",
.root_module = mod,
});
zigcrypto_test.root_module.addImport("sqlite", sqlite_mod);
const install = b.addInstallArtifact(zigcrypto_test, .{});
install.step.dependOn(&zigcrypto_test.step);
const run = b.addRunArtifact(zigcrypto_test);
run.step.dependOn(&zigcrypto_test.step);
return run;
}
// See https://www.sqlite.org/compile.html for flags
const EnableOptions = struct {
// https://www.sqlite.org/fts5.html
fts5: bool = false,
};
const PreprocessStep = struct {
const Config = struct {
source: std.Build.LazyPath,
target: std.Build.LazyPath,
};
step: std.Build.Step,
source: std.Build.LazyPath,
target: std.Build.LazyPath,
fn create(owner: *std.Build, config: Config) *PreprocessStep {
const step = owner.allocator.create(PreprocessStep) catch @panic("OOM");
step.* = .{
.step = std.Build.Step.init(.{
.id = std.Build.Step.Id.custom,
.name = "preprocess",
.owner = owner,
.makeFn = make,
}),
.source = config.source,
.target = config.target,
};
return step;
}
fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
const ps: *PreprocessStep = @fieldParentPtr("step", step);
const owner = step.owner;
const sqlite3_h = try ps.source.path(owner, "sqlite3.h").getPath3(owner, step).toString(owner.allocator);
const sqlite3ext_h = try ps.source.path(owner, "sqlite3ext.h").getPath3(owner, step).toString(owner.allocator);
const loadable_sqlite3_h = try ps.target.path(owner, "loadable-ext-sqlite3.h").getPath3(owner, step).toString(owner.allocator);
const loadable_sqlite3ext_h = try ps.target.path(owner, "loadable-ext-sqlite3ext.h").getPath3(owner, step).toString(owner.allocator);
var threaded: std.Io.Threaded = .init_single_threaded;
const io = threaded.io();
try Preprocessor.sqlite3(io, owner.allocator, sqlite3_h, loadable_sqlite3_h);
try Preprocessor.sqlite3ext(io, owner.allocator, sqlite3ext_h, loadable_sqlite3ext_h);
}
};

View File

@@ -0,0 +1,13 @@
.{
.name = .sqlite,
.fingerprint = 0xb8bb86826b7f6417,
.minimum_zig_version = "0.14.0",
.version = "3.48.0",
.dependencies = .{
.sqlite = .{
.url = "https://www.sqlite.org/2025/sqlite-amalgamation-3490200.zip",
.hash = "N-V-__8AAH-mpwB7g3MnqYU-ooUBF1t99RP27dZ9addtMVXD",
},
},
.paths = .{"."},
}

View File

@@ -0,0 +1,243 @@
const std = @import("std");
const debug = std.debug;
const mem = std.mem;
// This tool is used to preprocess the sqlite3 headers to make them usable to build loadable extensions.
//
// Due to limitations of `zig translate-c` (used by @cImport) the code produced by @cImport'ing the sqlite3ext.h header is unusable.
// The sqlite3ext.h header redefines the SQLite API like this:
//
// #define sqlite3_open_v2 sqlite3_api->open_v2
//
// This is not supported by `zig translate-c`, if there's already a definition for a function the aliasing macros won't do anything:
// translate-c keeps generating the code for the function defined in sqlite3.h
//
// Even if there's no definition already (we could for example remove the definition manually from the sqlite3.h file),
// the code generated fails to compile because it references the variable sqlite3_api which is not defined
//
// And even if the sqlite3_api is defined before, the generated code fails to compile because the functions are defined as consts and
// can only reference comptime stuff, however sqlite3_api is a runtime variable.
//
// The only viable option is to completely reomve the original function definitions and redefine all functions in Zig which forward
// calls to the sqlite3_api object.
//
// This works but it requires fairly extensive modifications of both sqlite3.h and sqlite3ext.h which is time consuming to do manually;
// this tool is intended to automate all these modifications.
fn readOriginalData(io: std.Io, allocator: mem.Allocator, path: []const u8) ![]const u8 {
var file = try std.Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);
var buf: [1024]u8 = undefined;
var reader = file.reader(io, &buf);
const data = reader.interface.readAlloc(allocator, 1024 * 1024);
return data;
}
const Processor = struct {
const Range = union(enum) {
delete: struct {
start: usize,
end: usize,
},
replace: struct {
start: usize,
end: usize,
replacement: []const u8,
},
};
allocator: mem.Allocator,
data: []const u8,
pos: usize,
range_start: usize,
ranges: std.ArrayList(Range),
fn init(allocator: mem.Allocator, data: []const u8) !Processor {
return .{
.allocator = allocator,
.data = data,
.pos = 0,
.range_start = 0,
.ranges = try std.ArrayList(Range).initCapacity(allocator, 4096),
};
}
fn readable(self: *Processor) []const u8 {
if (self.pos >= self.data.len) return "";
return self.data[self.pos..];
}
fn previousByte(self: *Processor) ?u8 {
if (self.pos <= 0) return null;
return self.data[self.pos - 1];
}
fn skipUntil(self: *Processor, needle: []const u8) bool {
const pos = mem.indexOfPos(u8, self.data, self.pos, needle);
if (pos) |p| {
self.pos = p;
return true;
}
return false;
}
fn consume(self: *Processor, needle: []const u8) void {
debug.assert(self.startsWith(needle));
self.pos += needle.len;
}
fn startsWith(self: *Processor, needle: []const u8) bool {
if (self.pos >= self.data.len) return false;
const data = self.data[self.pos..];
return mem.startsWith(u8, data, needle);
}
fn rangeStart(self: *Processor) void {
self.range_start = self.pos;
}
fn rangeDelete(self: *Processor) void {
self.ranges.appendAssumeCapacity(Range{
.delete = .{
.start = self.range_start,
.end = self.pos,
},
});
}
fn rangeReplace(self: *Processor, replacement: []const u8) void {
self.ranges.appendAssumeCapacity(Range{
.replace = .{
.start = self.range_start,
.end = self.pos,
.replacement = replacement,
},
});
}
fn dump(self: *Processor, writer: anytype) !void {
var pos: usize = 0;
for (self.ranges.items) |range| {
switch (range) {
.delete => |dr| {
const to_write = self.data[pos..dr.start];
try writer.interface.writeAll(to_write);
pos = dr.end;
},
.replace => |rr| {
const to_write = self.data[pos..rr.start];
try writer.interface.writeAll(to_write);
try writer.interface.writeAll(rr.replacement);
pos = rr.end;
},
}
// debug.print("excluded range: start={d} end={d} slice=\"{s}\"\n", .{
// range.start,
// range.end,
// processor.data[range.start..range.end],
// });
}
// Finally append the remaining data in the buffer (the last range will probably not be the end of the file)
if (pos < self.data.len) {
const remaining_data = self.data[pos..];
try writer.interface.writeAll(remaining_data);
}
}
};
pub fn sqlite3(io: std.Io, allocator: mem.Allocator, input_path: []const u8, output_path: []const u8) !void {
const data = try readOriginalData(io, allocator, input_path);
var processor = try Processor.init(allocator, data);
while (true) {
// Everything function definition is declared with SQLITE_API.
// Stop the loop if there's none in the remaining data.
if (!processor.skipUntil("SQLITE_API ")) break;
// If the byte just before is not a LN it's not a function definition.
// There are a couple instances where SQLITE_API appears in a comment.
const previous_byte = processor.previousByte() orelse 0;
if (previous_byte != '\n') {
processor.consume("SQLITE_API ");
continue;
}
// Now we assume we're at the start of a function definition.
//
// We keep track of every function definition by marking its start and end position in the data.
processor.rangeStart();
processor.consume("SQLITE_API ");
if (processor.startsWith("SQLITE_EXTERN ")) {
// This is not a function definition, ignore it.
// try processor.unmark();
continue;
}
_ = processor.skipUntil(");\n");
processor.consume(");\n");
processor.rangeDelete();
}
// Write the result
// FIXME: Handle this
var output_file = try std.Io.Dir.cwd().createFile(io, output_path, .{}); //0o644 });
defer output_file.close(io);
try output_file.writeStreamingAll(io, "/* sqlite3.h edited by the zig-sqlite build script */\n");
var buf: [1024]u8 = undefined;
var out_writer = output_file.writer(io, &buf);
try processor.dump(&out_writer);
}
pub fn sqlite3ext(io: std.Io, allocator: mem.Allocator, input_path: []const u8, output_path: []const u8) !void {
const data = try readOriginalData(io, allocator, input_path);
var processor = try Processor.init(allocator, data);
// Replace the include line
debug.assert(processor.skipUntil("#include \"sqlite3.h\""));
processor.rangeStart();
processor.consume("#include \"sqlite3.h\"");
processor.rangeReplace("#include \"loadable-ext-sqlite3.h\"");
// Delete all #define macros
while (true) {
if (!processor.skipUntil("#define sqlite3_")) break;
processor.rangeStart();
processor.consume("#define sqlite3_");
_ = processor.skipUntil("\n");
processor.consume("\n");
processor.rangeDelete();
}
// Write the result
// FIXME: File permissions
// var output_file = try std.fs.cwd().createFile(output_path, .{ .mode = 0o0644 });
var output_file = try std.Io.Dir.cwd().createFile(io, output_path, .{});
defer output_file.close(io);
try output_file.writeStreamingAll(io, "/* sqlite3ext.h edited by the zig-sqlite build script */\n");
var buf: [1024]u8 = undefined;
var out_writer = output_file.writer(io, &buf);
try processor.dump(&out_writer);
}

View File

@@ -0,0 +1,20 @@
const root = @import("root");
pub const c = if (@hasDecl(root, "loadable_extension"))
@import("c/loadable_extension.zig")
else
@cImport({
@cInclude("sqlite3.h");
@cInclude("workaround.h");
});
// versionGreaterThanOrEqualTo returns true if the SQLite version is >= to the major.minor.patch provided.
pub fn versionGreaterThanOrEqualTo(major: u8, minor: u8, patch: u8) bool {
return c.SQLITE_VERSION_NUMBER >= @as(u32, major) * 1000000 + @as(u32, minor) * 1000 + @as(u32, patch);
}
comptime {
if (!versionGreaterThanOrEqualTo(3, 21, 0)) {
@compileError("must use SQLite >= 3.21.0");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,447 @@
/* sqlite3ext.h edited by the zig-sqlite build script */
/*
** 2006 June 7
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
** This header file defines the SQLite interface for use by
** shared libraries that want to be imported as extensions into
** an SQLite instance. Shared libraries that intend to be loaded
** as extensions by SQLite should #include this file instead of
** sqlite3.h.
*/
#ifndef SQLITE3EXT_H
#define SQLITE3EXT_H
#include "loadable-ext-sqlite3.h"
/*
** The following structure holds pointers to all of the SQLite API
** routines.
**
** WARNING: In order to maintain backwards compatibility, add new
** interfaces to the end of this structure only. If you insert new
** interfaces in the middle of this structure, then older different
** versions of SQLite will not be able to load each other's shared
** libraries!
*/
struct sqlite3_api_routines {
void * (*aggregate_context)(sqlite3_context*,int nBytes);
int (*aggregate_count)(sqlite3_context*);
int (*bind_blob)(sqlite3_stmt*,int,const void*,int n,void(*)(void*));
int (*bind_double)(sqlite3_stmt*,int,double);
int (*bind_int)(sqlite3_stmt*,int,int);
int (*bind_int64)(sqlite3_stmt*,int,sqlite_int64);
int (*bind_null)(sqlite3_stmt*,int);
int (*bind_parameter_count)(sqlite3_stmt*);
int (*bind_parameter_index)(sqlite3_stmt*,const char*zName);
const char * (*bind_parameter_name)(sqlite3_stmt*,int);
int (*bind_text)(sqlite3_stmt*,int,const char*,int n,void(*)(void*));
int (*bind_text16)(sqlite3_stmt*,int,const void*,int,void(*)(void*));
int (*bind_value)(sqlite3_stmt*,int,const sqlite3_value*);
int (*busy_handler)(sqlite3*,int(*)(void*,int),void*);
int (*busy_timeout)(sqlite3*,int ms);
int (*changes)(sqlite3*);
int (*close)(sqlite3*);
int (*collation_needed)(sqlite3*,void*,void(*)(void*,sqlite3*,
int eTextRep,const char*));
int (*collation_needed16)(sqlite3*,void*,void(*)(void*,sqlite3*,
int eTextRep,const void*));
const void * (*column_blob)(sqlite3_stmt*,int iCol);
int (*column_bytes)(sqlite3_stmt*,int iCol);
int (*column_bytes16)(sqlite3_stmt*,int iCol);
int (*column_count)(sqlite3_stmt*pStmt);
const char * (*column_database_name)(sqlite3_stmt*,int);
const void * (*column_database_name16)(sqlite3_stmt*,int);
const char * (*column_decltype)(sqlite3_stmt*,int i);
const void * (*column_decltype16)(sqlite3_stmt*,int);
double (*column_double)(sqlite3_stmt*,int iCol);
int (*column_int)(sqlite3_stmt*,int iCol);
sqlite_int64 (*column_int64)(sqlite3_stmt*,int iCol);
const char * (*column_name)(sqlite3_stmt*,int);
const void * (*column_name16)(sqlite3_stmt*,int);
const char * (*column_origin_name)(sqlite3_stmt*,int);
const void * (*column_origin_name16)(sqlite3_stmt*,int);
const char * (*column_table_name)(sqlite3_stmt*,int);
const void * (*column_table_name16)(sqlite3_stmt*,int);
const unsigned char * (*column_text)(sqlite3_stmt*,int iCol);
const void * (*column_text16)(sqlite3_stmt*,int iCol);
int (*column_type)(sqlite3_stmt*,int iCol);
sqlite3_value* (*column_value)(sqlite3_stmt*,int iCol);
void * (*commit_hook)(sqlite3*,int(*)(void*),void*);
int (*complete)(const char*sql);
int (*complete16)(const void*sql);
int (*create_collation)(sqlite3*,const char*,int,void*,
int(*)(void*,int,const void*,int,const void*));
int (*create_collation16)(sqlite3*,const void*,int,void*,
int(*)(void*,int,const void*,int,const void*));
int (*create_function)(sqlite3*,const char*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*));
int (*create_function16)(sqlite3*,const void*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*));
int (*create_module)(sqlite3*,const char*,const sqlite3_module*,void*);
int (*data_count)(sqlite3_stmt*pStmt);
sqlite3 * (*db_handle)(sqlite3_stmt*);
int (*declare_vtab)(sqlite3*,const char*);
int (*enable_shared_cache)(int);
int (*errcode)(sqlite3*db);
const char * (*errmsg)(sqlite3*);
const void * (*errmsg16)(sqlite3*);
int (*exec)(sqlite3*,const char*,sqlite3_callback,void*,char**);
int (*expired)(sqlite3_stmt*);
int (*finalize)(sqlite3_stmt*pStmt);
void (*free)(void*);
void (*free_table)(char**result);
int (*get_autocommit)(sqlite3*);
void * (*get_auxdata)(sqlite3_context*,int);
int (*get_table)(sqlite3*,const char*,char***,int*,int*,char**);
int (*global_recover)(void);
void (*interruptx)(sqlite3*);
sqlite_int64 (*last_insert_rowid)(sqlite3*);
const char * (*libversion)(void);
int (*libversion_number)(void);
void *(*malloc)(int);
char * (*mprintf)(const char*,...);
int (*open)(const char*,sqlite3**);
int (*open16)(const void*,sqlite3**);
int (*prepare)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
int (*prepare16)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
void * (*profile)(sqlite3*,void(*)(void*,const char*,sqlite_uint64),void*);
void (*progress_handler)(sqlite3*,int,int(*)(void*),void*);
void *(*realloc)(void*,int);
int (*reset)(sqlite3_stmt*pStmt);
void (*result_blob)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_double)(sqlite3_context*,double);
void (*result_error)(sqlite3_context*,const char*,int);
void (*result_error16)(sqlite3_context*,const void*,int);
void (*result_int)(sqlite3_context*,int);
void (*result_int64)(sqlite3_context*,sqlite_int64);
void (*result_null)(sqlite3_context*);
void (*result_text)(sqlite3_context*,const char*,int,void(*)(void*));
void (*result_text16)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_text16be)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_text16le)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_value)(sqlite3_context*,sqlite3_value*);
void * (*rollback_hook)(sqlite3*,void(*)(void*),void*);
int (*set_authorizer)(sqlite3*,int(*)(void*,int,const char*,const char*,
const char*,const char*),void*);
void (*set_auxdata)(sqlite3_context*,int,void*,void (*)(void*));
char * (*xsnprintf)(int,char*,const char*,...);
int (*step)(sqlite3_stmt*);
int (*table_column_metadata)(sqlite3*,const char*,const char*,const char*,
char const**,char const**,int*,int*,int*);
void (*thread_cleanup)(void);
int (*total_changes)(sqlite3*);
void * (*trace)(sqlite3*,void(*xTrace)(void*,const char*),void*);
int (*transfer_bindings)(sqlite3_stmt*,sqlite3_stmt*);
void * (*update_hook)(sqlite3*,void(*)(void*,int ,char const*,char const*,
sqlite_int64),void*);
void * (*user_data)(sqlite3_context*);
const void * (*value_blob)(sqlite3_value*);
int (*value_bytes)(sqlite3_value*);
int (*value_bytes16)(sqlite3_value*);
double (*value_double)(sqlite3_value*);
int (*value_int)(sqlite3_value*);
sqlite_int64 (*value_int64)(sqlite3_value*);
int (*value_numeric_type)(sqlite3_value*);
const unsigned char * (*value_text)(sqlite3_value*);
const void * (*value_text16)(sqlite3_value*);
const void * (*value_text16be)(sqlite3_value*);
const void * (*value_text16le)(sqlite3_value*);
int (*value_type)(sqlite3_value*);
char *(*vmprintf)(const char*,va_list);
/* Added ??? */
int (*overload_function)(sqlite3*, const char *zFuncName, int nArg);
/* Added by 3.3.13 */
int (*prepare_v2)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
int (*prepare16_v2)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
int (*clear_bindings)(sqlite3_stmt*);
/* Added by 3.4.1 */
int (*create_module_v2)(sqlite3*,const char*,const sqlite3_module*,void*,
void (*xDestroy)(void *));
/* Added by 3.5.0 */
int (*bind_zeroblob)(sqlite3_stmt*,int,int);
int (*blob_bytes)(sqlite3_blob*);
int (*blob_close)(sqlite3_blob*);
int (*blob_open)(sqlite3*,const char*,const char*,const char*,sqlite3_int64,
int,sqlite3_blob**);
int (*blob_read)(sqlite3_blob*,void*,int,int);
int (*blob_write)(sqlite3_blob*,const void*,int,int);
int (*create_collation_v2)(sqlite3*,const char*,int,void*,
int(*)(void*,int,const void*,int,const void*),
void(*)(void*));
int (*file_control)(sqlite3*,const char*,int,void*);
sqlite3_int64 (*memory_highwater)(int);
sqlite3_int64 (*memory_used)(void);
sqlite3_mutex *(*mutex_alloc)(int);
void (*mutex_enter)(sqlite3_mutex*);
void (*mutex_free)(sqlite3_mutex*);
void (*mutex_leave)(sqlite3_mutex*);
int (*mutex_try)(sqlite3_mutex*);
int (*open_v2)(const char*,sqlite3**,int,const char*);
int (*release_memory)(int);
void (*result_error_nomem)(sqlite3_context*);
void (*result_error_toobig)(sqlite3_context*);
int (*sleep)(int);
void (*soft_heap_limit)(int);
sqlite3_vfs *(*vfs_find)(const char*);
int (*vfs_register)(sqlite3_vfs*,int);
int (*vfs_unregister)(sqlite3_vfs*);
int (*xthreadsafe)(void);
void (*result_zeroblob)(sqlite3_context*,int);
void (*result_error_code)(sqlite3_context*,int);
int (*test_control)(int, ...);
void (*randomness)(int,void*);
sqlite3 *(*context_db_handle)(sqlite3_context*);
int (*extended_result_codes)(sqlite3*,int);
int (*limit)(sqlite3*,int,int);
sqlite3_stmt *(*next_stmt)(sqlite3*,sqlite3_stmt*);
const char *(*sql)(sqlite3_stmt*);
int (*status)(int,int*,int*,int);
int (*backup_finish)(sqlite3_backup*);
sqlite3_backup *(*backup_init)(sqlite3*,const char*,sqlite3*,const char*);
int (*backup_pagecount)(sqlite3_backup*);
int (*backup_remaining)(sqlite3_backup*);
int (*backup_step)(sqlite3_backup*,int);
const char *(*compileoption_get)(int);
int (*compileoption_used)(const char*);
int (*create_function_v2)(sqlite3*,const char*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void(*xDestroy)(void*));
int (*db_config)(sqlite3*,int,...);
sqlite3_mutex *(*db_mutex)(sqlite3*);
int (*db_status)(sqlite3*,int,int*,int*,int);
int (*extended_errcode)(sqlite3*);
void (*log)(int,const char*,...);
sqlite3_int64 (*soft_heap_limit64)(sqlite3_int64);
const char *(*sourceid)(void);
int (*stmt_status)(sqlite3_stmt*,int,int);
int (*strnicmp)(const char*,const char*,int);
int (*unlock_notify)(sqlite3*,void(*)(void**,int),void*);
int (*wal_autocheckpoint)(sqlite3*,int);
int (*wal_checkpoint)(sqlite3*,const char*);
void *(*wal_hook)(sqlite3*,int(*)(void*,sqlite3*,const char*,int),void*);
int (*blob_reopen)(sqlite3_blob*,sqlite3_int64);
int (*vtab_config)(sqlite3*,int op,...);
int (*vtab_on_conflict)(sqlite3*);
/* Version 3.7.16 and later */
int (*close_v2)(sqlite3*);
const char *(*db_filename)(sqlite3*,const char*);
int (*db_readonly)(sqlite3*,const char*);
int (*db_release_memory)(sqlite3*);
const char *(*errstr)(int);
int (*stmt_busy)(sqlite3_stmt*);
int (*stmt_readonly)(sqlite3_stmt*);
int (*stricmp)(const char*,const char*);
int (*uri_boolean)(const char*,const char*,int);
sqlite3_int64 (*uri_int64)(const char*,const char*,sqlite3_int64);
const char *(*uri_parameter)(const char*,const char*);
char *(*xvsnprintf)(int,char*,const char*,va_list);
int (*wal_checkpoint_v2)(sqlite3*,const char*,int,int*,int*);
/* Version 3.8.7 and later */
int (*auto_extension)(void(*)(void));
int (*bind_blob64)(sqlite3_stmt*,int,const void*,sqlite3_uint64,
void(*)(void*));
int (*bind_text64)(sqlite3_stmt*,int,const char*,sqlite3_uint64,
void(*)(void*),unsigned char);
int (*cancel_auto_extension)(void(*)(void));
int (*load_extension)(sqlite3*,const char*,const char*,char**);
void *(*malloc64)(sqlite3_uint64);
sqlite3_uint64 (*msize)(void*);
void *(*realloc64)(void*,sqlite3_uint64);
void (*reset_auto_extension)(void);
void (*result_blob64)(sqlite3_context*,const void*,sqlite3_uint64,
void(*)(void*));
void (*result_text64)(sqlite3_context*,const char*,sqlite3_uint64,
void(*)(void*), unsigned char);
int (*strglob)(const char*,const char*);
/* Version 3.8.11 and later */
sqlite3_value *(*value_dup)(const sqlite3_value*);
void (*value_free)(sqlite3_value*);
int (*result_zeroblob64)(sqlite3_context*,sqlite3_uint64);
int (*bind_zeroblob64)(sqlite3_stmt*, int, sqlite3_uint64);
/* Version 3.9.0 and later */
unsigned int (*value_subtype)(sqlite3_value*);
void (*result_subtype)(sqlite3_context*,unsigned int);
/* Version 3.10.0 and later */
int (*status64)(int,sqlite3_int64*,sqlite3_int64*,int);
int (*strlike)(const char*,const char*,unsigned int);
int (*db_cacheflush)(sqlite3*);
/* Version 3.12.0 and later */
int (*system_errno)(sqlite3*);
/* Version 3.14.0 and later */
int (*trace_v2)(sqlite3*,unsigned,int(*)(unsigned,void*,void*,void*),void*);
char *(*expanded_sql)(sqlite3_stmt*);
/* Version 3.18.0 and later */
void (*set_last_insert_rowid)(sqlite3*,sqlite3_int64);
/* Version 3.20.0 and later */
int (*prepare_v3)(sqlite3*,const char*,int,unsigned int,
sqlite3_stmt**,const char**);
int (*prepare16_v3)(sqlite3*,const void*,int,unsigned int,
sqlite3_stmt**,const void**);
int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
void *(*value_pointer)(sqlite3_value*,const char*);
int (*vtab_nochange)(sqlite3_context*);
int (*value_nochange)(sqlite3_value*);
const char *(*vtab_collation)(sqlite3_index_info*,int);
/* Version 3.24.0 and later */
int (*keyword_count)(void);
int (*keyword_name)(int,const char**,int*);
int (*keyword_check)(const char*,int);
sqlite3_str *(*str_new)(sqlite3*);
char *(*str_finish)(sqlite3_str*);
void (*str_appendf)(sqlite3_str*, const char *zFormat, ...);
void (*str_vappendf)(sqlite3_str*, const char *zFormat, va_list);
void (*str_append)(sqlite3_str*, const char *zIn, int N);
void (*str_appendall)(sqlite3_str*, const char *zIn);
void (*str_appendchar)(sqlite3_str*, int N, char C);
void (*str_reset)(sqlite3_str*);
int (*str_errcode)(sqlite3_str*);
int (*str_length)(sqlite3_str*);
char *(*str_value)(sqlite3_str*);
/* Version 3.25.0 and later */
int (*create_window_function)(sqlite3*,const char*,int,int,void*,
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void (*xValue)(sqlite3_context*),
void (*xInv)(sqlite3_context*,int,sqlite3_value**),
void(*xDestroy)(void*));
/* Version 3.26.0 and later */
const char *(*normalized_sql)(sqlite3_stmt*);
/* Version 3.28.0 and later */
int (*stmt_isexplain)(sqlite3_stmt*);
int (*value_frombind)(sqlite3_value*);
/* Version 3.30.0 and later */
int (*drop_modules)(sqlite3*,const char**);
/* Version 3.31.0 and later */
sqlite3_int64 (*hard_heap_limit64)(sqlite3_int64);
const char *(*uri_key)(const char*,int);
const char *(*filename_database)(const char*);
const char *(*filename_journal)(const char*);
const char *(*filename_wal)(const char*);
/* Version 3.32.0 and later */
const char *(*create_filename)(const char*,const char*,const char*,
int,const char**);
void (*free_filename)(const char*);
sqlite3_file *(*database_file_object)(const char*);
/* Version 3.34.0 and later */
int (*txn_state)(sqlite3*,const char*);
/* Version 3.36.1 and later */
sqlite3_int64 (*changes64)(sqlite3*);
sqlite3_int64 (*total_changes64)(sqlite3*);
/* Version 3.37.0 and later */
int (*autovacuum_pages)(sqlite3*,
unsigned int(*)(void*,const char*,unsigned int,unsigned int,unsigned int),
void*, void(*)(void*));
/* Version 3.38.0 and later */
int (*error_offset)(sqlite3*);
int (*vtab_rhs_value)(sqlite3_index_info*,int,sqlite3_value**);
int (*vtab_distinct)(sqlite3_index_info*);
int (*vtab_in)(sqlite3_index_info*,int,int);
int (*vtab_in_first)(sqlite3_value*,sqlite3_value**);
int (*vtab_in_next)(sqlite3_value*,sqlite3_value**);
/* Version 3.39.0 and later */
int (*deserialize)(sqlite3*,const char*,unsigned char*,
sqlite3_int64,sqlite3_int64,unsigned);
unsigned char *(*serialize)(sqlite3*,const char *,sqlite3_int64*,
unsigned int);
const char *(*db_name)(sqlite3*,int);
/* Version 3.40.0 and later */
int (*value_encoding)(sqlite3_value*);
/* Version 3.41.0 and later */
int (*is_interrupted)(sqlite3*);
/* Version 3.43.0 and later */
int (*stmt_explain)(sqlite3_stmt*,int);
/* Version 3.44.0 and later */
void *(*get_clientdata)(sqlite3*,const char*);
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
};
/*
** This is the function signature used for all extension entry points. It
** is also defined in the file "loadext.c".
*/
typedef int (*sqlite3_loadext_entry)(
sqlite3 *db, /* Handle to the database. */
char **pzErrMsg, /* Used to set error string on failure. */
const sqlite3_api_routines *pThunk /* Extension API function pointers. */
);
/*
** The following macros redefine the API routines so that they are
** redirected through the global sqlite3_api structure.
**
** This header file is also used by the loadext.c source file
** (part of the main SQLite library - not an extension) so that
** it can get access to the sqlite3_api_routines structure
** definition. But the main library does not want to redefine
** the API. So the redefinition macros are only valid if the
** SQLITE_CORE macros is undefined.
*/
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
#ifndef SQLITE_OMIT_DEPRECATED
#endif
#ifndef SQLITE_OMIT_DEPRECATED
#endif
#ifndef SQLITE_OMIT_DEPRECATED
#endif
#ifndef SQLITE_OMIT_DEPRECATED
#endif
/* Version 3.7.16 and later */
/* Version 3.8.7 and later */
/* Version 3.8.11 and later */
/* Version 3.9.0 and later */
/* Version 3.10.0 and later */
/* Version 3.12.0 and later */
/* Version 3.14.0 and later */
/* Version 3.18.0 and later */
/* Version 3.20.0 and later */
/* Version 3.22.0 and later */
/* Version 3.24.0 and later */
/* Version 3.25.0 and later */
/* Version 3.26.0 and later */
/* Version 3.28.0 and later */
/* Version 3.30.0 and later */
/* Version 3.31.0 and later */
/* Version 3.32.0 and later */
/* Version 3.34.0 and later */
/* Version 3.36.1 and later */
/* Version 3.37.0 and later */
/* Version 3.38.0 and later */
/* Version 3.39.0 and later */
#ifndef SQLITE_OMIT_DESERIALIZE
#endif
/* Version 3.40.0 and later */
/* Version 3.41.0 and later */
/* Version 3.43.0 and later */
/* Version 3.44.0 and later */
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
/* This case when the file really is being compiled as a loadable
** extension */
# define SQLITE_EXTENSION_INIT1 const sqlite3_api_routines *sqlite3_api=0;
# define SQLITE_EXTENSION_INIT2(v) sqlite3_api=v;
# define SQLITE_EXTENSION_INIT3 \
extern const sqlite3_api_routines *sqlite3_api;
#else
/* This case when the file is being statically linked into the
** application */
# define SQLITE_EXTENSION_INIT1 /*no-op*/
# define SQLITE_EXTENSION_INIT2(v) (void)v; /* unused parameter */
# define SQLITE_EXTENSION_INIT3 /*no-op*/
#endif
#endif /* SQLITE3EXT_H */

View File

@@ -0,0 +1,808 @@
pub const c = @cImport({
@cInclude("loadable-ext-sqlite3ext.h");
@cInclude("workaround.h");
});
pub var sqlite3_api: [*c]c.sqlite3_api_routines = null;
pub const sqlite3_transfer_bindings = @compileError("sqlite3_transfer_bindings is deprecated");
pub const sqlite3_global_recover = @compileError("sqlite3_global_recover is deprecated");
pub const sqlite3_expired = @compileError("sqlite3_expired is deprecated");
pub const sqlite3_mprintf = @compileError("sqlite3_mprintf can't be implemented in Zig");
pub const sqlite3_snprintf = @compileError("sqlite3_snprintf can't be implemented in Zig");
pub const sqlite3_vmprintf = @compileError("sqlite3_vmprintf can't be implemented in Zig");
pub const sqlite3_vsnprintf = @compileError("sqlite3_vsnprintf can't be implemented in Zig");
pub const sqlite3_test_control = @compileError("sqlite3_test_control can't be implemented in Zig");
pub const sqlite3_db_config = @compileError("sqlite3_db_config can't be implemented in Zig");
pub const sqlite3_log = @compileError("sqlite3_log can't be implemented in Zig");
pub const sqlite3_vtab_config = @compileError("sqlite3_vtab_config can't be implemented in Zig");
pub const sqlite3_uri_vsnprintf = @compileError("sqlite3_uri_vsnprintf can't be implemented in Zig");
pub const sqlite3_str_appendf = @compileError("sqlite3_str_appendf can't be implemented in Zig");
pub const sqlite3_str_vappendf = @compileError("sqlite3_str_vappendf can't be implemented in Zig");
pub export fn sqlite3_aggregate_context(p: ?*c.sqlite3_context, nBytes: c_int) callconv(.c) ?*anyopaque {
return sqlite3_api.*.aggregate_context.?(p, nBytes);
}
pub export fn sqlite3_bind_blob(pStmt: ?*c.sqlite3_stmt, i: c_int, zData: ?*const anyopaque, nData: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.bind_blob.?(pStmt, i, zData, nData, xDel);
}
pub export fn sqlite3_bind_double(pStmt: ?*c.sqlite3_stmt, i: c_int, rValue: f64) callconv(.c) c_int {
return sqlite3_api.*.bind_double.?(pStmt, i, rValue);
}
pub export fn sqlite3_bind_int(pStmt: ?*c.sqlite3_stmt, i: c_int, iValue: c_int) callconv(.c) c_int {
return sqlite3_api.*.bind_int.?(pStmt, i, iValue);
}
pub export fn sqlite3_bind_int64(pStmt: ?*c.sqlite3_stmt, i: c_int, iValue: c.sqlite3_int64) c_int {
return sqlite3_api.*.bind_int64.?(pStmt, i, iValue);
}
pub export fn sqlite3_bind_null(pStmt: ?*c.sqlite3_stmt, i: c_int) c_int {
return sqlite3_api.*.bind_null.?(pStmt, i);
}
pub export fn sqlite3_bind_parameter_count(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.bind_parameter_count.?(pStmt);
}
pub export fn sqlite3_bind_parameter_index(pStmt: ?*c.sqlite3_stmt, zName: [*c]const u8) c_int {
return sqlite3_api.*.bind_parameter_index.?(pStmt, zName);
}
pub export fn sqlite3_bind_parameter_name(pStmt: ?*c.sqlite3_stmt, i: c_int) [*c]const u8 {
return sqlite3_api.*.bind_parameter_name.?(pStmt, i);
}
pub export fn sqlite3_bind_text(pStmt: ?*c.sqlite3_stmt, i: c_int, zData: [*c]const u8, nData: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.bind_text.?(pStmt, i, zData, nData, xDel);
}
pub export fn sqlite3_bind_text16(pStmt: ?*c.sqlite3_stmt, i: c_int, zData: ?*const anyopaque, nData: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.bind_text16.?(pStmt, i, zData, nData, xDel);
}
pub export fn sqlite3_bind_value(pStmt: ?*c.sqlite3_stmt, i: c_int, pValue: ?*const c.sqlite3_value) c_int {
return sqlite3_api.*.bind_value.?(pStmt, i, pValue);
}
pub export fn sqlite3_busy_handler(db: ?*c.sqlite3, xBusy: ?*const fn (?*anyopaque, c_int) callconv(.c) c_int, pArg: ?*anyopaque) c_int {
return sqlite3_api.*.busy_handler.?(db, xBusy, pArg);
}
pub export fn sqlite3_busy_timeout(db: ?*c.sqlite3, ms: c_int) c_int {
return sqlite3_api.*.busy_timeout.?(db, ms);
}
pub export fn sqlite3_changes(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.changes.?(db);
}
pub export fn sqlite3_close(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.close.?(db);
}
pub export fn sqlite3_collation_needed(db: ?*c.sqlite3, pCollNeededArg: ?*anyopaque, xCollNeeded: ?*const fn (?*anyopaque, ?*c.sqlite3, c_int, [*c]const u8) callconv(.c) void) c_int {
return sqlite3_api.*.collation_needed.?(db, pCollNeededArg, xCollNeeded);
}
pub export fn sqlite3_collation_needed16(db: ?*c.sqlite3, pCollNeededArg: ?*anyopaque, xCollNeeded16: ?*const fn (?*anyopaque, ?*c.sqlite3, c_int, ?*const anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.collation_needed16.?(db, pCollNeededArg, xCollNeeded16);
}
pub export fn sqlite3_column_blob(pStmt: ?*c.sqlite3_stmt, iCol: c_int) ?*const anyopaque {
return sqlite3_api.*.column_blob.?(pStmt, iCol);
}
pub export fn sqlite3_column_bytes(pStmt: ?*c.sqlite3_stmt, iCol: c_int) c_int {
return sqlite3_api.*.column_bytes.?(pStmt, iCol);
}
pub export fn sqlite3_column_bytes16(pStmt: ?*c.sqlite3_stmt, iCol: c_int) c_int {
return sqlite3_api.*.column_bytes16.?(pStmt, iCol);
}
pub export fn sqlite3_column_count(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.column_count.?(pStmt);
}
pub export fn sqlite3_column_database_name(pStmt: ?*c.sqlite3_stmt, iCol: c_int) [*c]const u8 {
return sqlite3_api.*.column_database_name.?(pStmt, iCol);
}
pub export fn sqlite3_column_database_name16(pStmt: ?*c.sqlite3_stmt, iCol: c_int) ?*const anyopaque {
return sqlite3_api.*.column_database_name16.?(pStmt, iCol);
}
pub export fn sqlite3_column_decltype(pStmt: ?*c.sqlite3_stmt, iCol: c_int) [*c]const u8 {
return sqlite3_api.*.column_decltype.?(pStmt, iCol);
}
pub export fn sqlite3_column_decltype16(pStmt: ?*c.sqlite3_stmt, iCol: c_int) ?*const anyopaque {
return sqlite3_api.*.column_decltype16.?(pStmt, iCol);
}
pub export fn sqlite3_column_double(pStmt: ?*c.sqlite3_stmt, iCol: c_int) f64 {
return sqlite3_api.*.column_double.?(pStmt, iCol);
}
pub export fn sqlite3_column_int(pStmt: ?*c.sqlite3_stmt, iCol: c_int) c_int {
return sqlite3_api.*.column_int.?(pStmt, iCol);
}
pub export fn sqlite3_column_int64(pStmt: ?*c.sqlite3_stmt, iCol: c_int) c.sqlite3_int64 {
return sqlite3_api.*.column_int64.?(pStmt, iCol);
}
pub export fn sqlite3_column_name(pStmt: ?*c.sqlite3_stmt, N: c_int) [*c]const u8 {
return sqlite3_api.*.column_name.?(pStmt, N);
}
pub export fn sqlite3_column_name16(pStmt: ?*c.sqlite3_stmt, N: c_int) ?*const anyopaque {
return sqlite3_api.*.column_name16.?(pStmt, N);
}
pub export fn sqlite3_column_origin_name(pStmt: ?*c.sqlite3_stmt, N: c_int) [*c]const u8 {
return sqlite3_api.*.column_origin_name.?(pStmt, N);
}
pub export fn sqlite3_column_origin_name16(pStmt: ?*c.sqlite3_stmt, N: c_int) ?*const anyopaque {
return sqlite3_api.*.column_origin_name16.?(pStmt, N);
}
pub export fn sqlite3_column_table_name(pStmt: ?*c.sqlite3_stmt, N: c_int) [*c]const u8 {
return sqlite3_api.*.column_table_name.?(pStmt, N);
}
pub export fn sqlite3_column_table_name16(pStmt: ?*c.sqlite3_stmt, N: c_int) ?*const anyopaque {
return sqlite3_api.*.column_table_name16.?(pStmt, N);
}
pub export fn sqlite3_column_text(pStmt: ?*c.sqlite3_stmt, iCol: c_int) [*c]const u8 {
return sqlite3_api.*.column_text.?(pStmt, iCol);
}
pub export fn sqlite3_column_text16(pStmt: ?*c.sqlite3_stmt, iCol: c_int) ?*const anyopaque {
return sqlite3_api.*.column_text16.?(pStmt, iCol);
}
pub export fn sqlite3_column_type(pStmt: ?*c.sqlite3_stmt, iCol: c_int) c_int {
return sqlite3_api.*.column_type.?(pStmt, iCol);
}
pub export fn sqlite3_column_value(pStmt: ?*c.sqlite3_stmt, iCol: c_int) ?*c.sqlite3_value {
return sqlite3_api.*.column_value.?(pStmt, iCol);
}
pub export fn sqlite3_commit_hook(db: ?*c.sqlite3, xCallback: ?*const fn (?*anyopaque) callconv(.c) c_int, pArg: ?*anyopaque) ?*anyopaque {
return sqlite3_api.*.commit_hook.?(db, xCallback, pArg);
}
pub export fn sqlite3_complete(sql: [*c]const u8) c_int {
return sqlite3_api.*.complete.?(sql);
}
pub export fn sqlite3_complete16(sql: ?*const anyopaque) c_int {
return sqlite3_api.*.complete16.?(sql);
}
pub export fn sqlite3_create_collation(db: ?*c.sqlite3, zName: [*c]const u8, eTextRep: c_int, pArg: ?*anyopaque, xCompare: ?*const fn (?*anyopaque, c_int, ?*const anyopaque, c_int, ?*const anyopaque) callconv(.c) c_int) c_int {
return sqlite3_api.*.create_collation.?(db, zName, eTextRep, pArg, xCompare);
}
pub export fn sqlite3_create_collation16(db: ?*c.sqlite3, zName: ?*const anyopaque, eTextRep: c_int, pArg: ?*anyopaque, xCompare: ?*const fn (?*anyopaque, c_int, ?*const anyopaque, c_int, ?*const anyopaque) callconv(.c) c_int) c_int {
return sqlite3_api.*.create_collation16.?(db, zName, eTextRep, pArg, xCompare);
}
pub export fn sqlite3_create_function(db: ?*c.sqlite3, zFunctionName: [*c]const u8, nArg: c_int, eTextRep: c_int, pApp: ?*anyopaque, xFunc: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void, xStep: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void, xFinal: ?*const fn (?*c.sqlite3_context) callconv(.c) void) c_int {
return sqlite3_api.*.create_function.?(db, zFunctionName, nArg, eTextRep, pApp, xFunc, xStep, xFinal);
}
pub export fn sqlite3_create_function16(db: ?*c.sqlite3, zFunctionName: ?*const anyopaque, nArg: c_int, eTextRep: c_int, pApp: ?*anyopaque, xFunc: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void, xStep: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void, xFinal: ?*const fn (?*c.sqlite3_context) callconv(.c) void) c_int {
return sqlite3_api.*.create_function16.?(db, zFunctionName, nArg, eTextRep, pApp, xFunc, xStep, xFinal);
}
pub export fn sqlite3_create_module(db: ?*c.sqlite3, zName: [*c]const u8, pModule: [*c]const c.sqlite3_module, pAux: ?*anyopaque) c_int {
return sqlite3_api.*.create_module.?(db, zName, pModule, pAux);
}
pub export fn sqlite3_create_module_v2(db: ?*c.sqlite3, zName: [*c]const u8, pModule: [*c]const c.sqlite3_module, pAux: ?*anyopaque, xDestroy: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.create_module_v2.?(db, zName, pModule, pAux, xDestroy);
}
pub export fn sqlite3_data_count(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.data_count.?(pStmt);
}
pub export fn sqlite3_db_handle(pStmt: ?*c.sqlite3_stmt) ?*c.sqlite3 {
return sqlite3_api.*.db_handle.?(pStmt);
}
pub export fn sqlite3_declare_vtab(db: ?*c.sqlite3, zSQL: [*c]const u8) c_int {
return sqlite3_api.*.declare_vtab.?(db, zSQL);
}
pub export fn sqlite3_enable_shared_cache(enable: c_int) c_int {
return sqlite3_api.*.enable_shared_cache.?(enable);
}
pub export fn sqlite3_errcode(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.errcode.?(db);
}
pub export fn sqlite3_errmsg(db: ?*c.sqlite3) [*c]const u8 {
return sqlite3_api.*.errmsg.?(db);
}
pub export fn sqlite3_errmsg16(db: ?*c.sqlite3) ?*const anyopaque {
return sqlite3_api.*.errmsg16.?(db);
}
pub export fn sqlite3_exec(db: ?*c.sqlite3, zSql: [*c]const u8, xCallback: ?*const fn (?*anyopaque, c_int, [*c][*c]u8, [*c][*c]u8) callconv(.c) c_int, pArg: ?*anyopaque, pzErrMsg: [*c][*c]u8) c_int {
return sqlite3_api.*.exec.?(db, zSql, xCallback, pArg, pzErrMsg);
}
pub export fn sqlite3_finalize(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.finalize.?(pStmt);
}
pub export fn sqlite3_free(p: ?*anyopaque) void {
return sqlite3_api.*.free.?(p);
}
pub export fn sqlite3_free_table(result: [*c][*c]u8) void {
return sqlite3_api.*.free_table.?(result);
}
pub export fn sqlite3_get_autocommit(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.get_autocommit.?(db);
}
pub export fn sqlite3_get_auxdata(pCtx: ?*c.sqlite3_context, iArg: c_int) ?*anyopaque {
return sqlite3_api.*.get_auxdata.?(pCtx, iArg);
}
pub export fn sqlite3_get_table(db: ?*c.sqlite3, zSql: [*c]const u8, pazResult: [*c][*c][*c]u8, pnRow: [*c]c_int, pnColumn: [*c]c_int, pzErrMsg: [*c][*c]u8) c_int {
return sqlite3_api.*.get_table.?(db, zSql, pazResult, pnRow, pnColumn, pzErrMsg);
}
pub export fn sqlite3_interrupt(db: ?*c.sqlite3) void {
return sqlite3_api.*.interruptx.?(db);
}
pub export fn sqlite3_last_insert_rowid(db: ?*c.sqlite3) c.sqlite3_int64 {
return sqlite3_api.*.last_insert_rowid.?(db);
}
pub export fn sqlite3_libversion() callconv(.c) [*c]const u8 {
return sqlite3_api.*.libversion.?();
}
pub export fn sqlite3_libversion_number() c_int {
return sqlite3_api.*.libversion_number.?();
}
pub export fn sqlite3_malloc(n: c_int) ?*anyopaque {
return sqlite3_api.*.malloc.?(n);
}
pub export fn sqlite3_open(filename: [*c]const u8, ppDb: [*c]?*c.sqlite3) c_int {
return sqlite3_api.*.open.?(filename, ppDb);
}
pub export fn sqlite3_open16(filename: ?*const anyopaque, ppDb: [*c]?*c.sqlite3) c_int {
return sqlite3_api.*.open16.?(filename, ppDb);
}
pub export fn sqlite3_prepare(db: ?*c.sqlite3, zSql: [*c]const u8, nByte: c_int, ppStmt: [*c]?*c.sqlite3_stmt, pzTail: [*c][*c]const u8) c_int {
return sqlite3_api.*.prepare.?(db, zSql, nByte, ppStmt, pzTail);
}
pub export fn sqlite3_prepare16(db: ?*c.sqlite3, zSql: ?*const anyopaque, nByte: c_int, ppStmt: [*c]?*c.sqlite3_stmt, pzTail: [*c]?*const anyopaque) c_int {
return sqlite3_api.*.prepare16.?(db, zSql, nByte, ppStmt, pzTail);
}
pub export fn sqlite3_prepare_v2(db: ?*c.sqlite3, zSql: [*c]const u8, nByte: c_int, ppStmt: [*c]?*c.sqlite3_stmt, pzTail: [*c][*c]const u8) c_int {
return sqlite3_api.*.prepare_v2.?(db, zSql, nByte, ppStmt, pzTail);
}
pub export fn sqlite3_prepare16_v2(db: ?*c.sqlite3, zSql: ?*const anyopaque, nByte: c_int, ppStmt: [*c]?*c.sqlite3_stmt, pzTail: [*c]?*const anyopaque) c_int {
return sqlite3_api.*.prepare16_v2.?(db, zSql, nByte, ppStmt, pzTail);
}
pub export fn sqlite3_profile(db: ?*c.sqlite3, xProfile: ?*const fn (?*anyopaque, [*c]const u8, c.sqlite3_uint64) callconv(.c) void, pArg: ?*anyopaque) ?*anyopaque {
return sqlite3_api.*.profile.?(db, xProfile, pArg);
}
pub export fn sqlite3_progress_handler(db: ?*c.sqlite3, nOps: c_int, xProgress: ?*const fn (?*anyopaque) callconv(.c) c_int, pArg: ?*anyopaque) void {
return sqlite3_api.*.progress_handler.?(db, nOps, xProgress, pArg);
}
pub export fn sqlite3_realloc(pOld: ?*anyopaque, n: c_int) ?*anyopaque {
return sqlite3_api.*.realloc.?(pOld, n);
}
pub export fn sqlite3_reset(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.reset.?(pStmt);
}
pub export fn sqlite3_result_blob(pCtx: ?*c.sqlite3_context, z: ?*const anyopaque, n: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_blob.?(pCtx, z, n, xDel);
}
pub export fn sqlite3_result_double(pCtx: ?*c.sqlite3_context, rVal: f64) void {
return sqlite3_api.*.result_double.?(pCtx, rVal);
}
pub export fn sqlite3_result_error(pCtx: ?*c.sqlite3_context, z: [*c]const u8, n: c_int) void {
return sqlite3_api.*.result_error.?(pCtx, z, n);
}
pub export fn sqlite3_result_error16(pCtx: ?*c.sqlite3_context, z: ?*const anyopaque, n: c_int) void {
return sqlite3_api.*.result_error16.?(pCtx, z, n);
}
pub export fn sqlite3_result_int(pCtx: ?*c.sqlite3_context, iVal: c_int) void {
return sqlite3_api.*.result_int.?(pCtx, iVal);
}
pub export fn sqlite3_result_int64(pCtx: ?*c.sqlite3_context, iVal: c.sqlite3_int64) void {
return sqlite3_api.*.result_int64.?(pCtx, iVal);
}
pub export fn sqlite3_result_null(pCtx: ?*c.sqlite3_context) void {
return sqlite3_api.*.result_null.?(pCtx);
}
pub export fn sqlite3_result_text(pCtx: ?*c.sqlite3_context, z: [*c]const u8, n: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_text.?(pCtx, z, n, xDel);
}
pub export fn sqlite3_result_text16(pCtx: ?*c.sqlite3_context, z: ?*const anyopaque, n: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_text16.?(pCtx, z, n, xDel);
}
pub export fn sqlite3_result_text16be(pCtx: ?*c.sqlite3_context, z: ?*const anyopaque, n: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_text16be.?(pCtx, z, n, xDel);
}
pub export fn sqlite3_result_text16le(pCtx: ?*c.sqlite3_context, z: ?*const anyopaque, n: c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_text16le.?(pCtx, z, n, xDel);
}
pub export fn sqlite3_result_value(pCtx: ?*c.sqlite3_context, pValue: ?*c.sqlite3_value) void {
return sqlite3_api.*.result_value.?(pCtx, pValue);
}
pub export fn sqlite3_rollback_hook(db: ?*c.sqlite3, xCallback: ?*const fn (?*anyopaque) callconv(.c) void, pArg: ?*anyopaque) ?*anyopaque {
return sqlite3_api.*.rollback_hook.?(db, xCallback, pArg);
}
pub export fn sqlite3_set_authorizer(db: ?*c.sqlite3, xAuth: ?*const fn (?*anyopaque, c_int, [*c]const u8, [*c]const u8, [*c]const u8, [*c]const u8) callconv(.c) c_int, pArg: ?*anyopaque) c_int {
return sqlite3_api.*.set_authorizer.?(db, xAuth, pArg);
}
pub export fn sqlite3_set_auxdata(pCtx: ?*c.sqlite3_context, iArg: c_int, pAux: ?*anyopaque, xDelete: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.set_auxdata.?(pCtx, iArg, pAux, xDelete);
}
pub export fn sqlite3_step(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.step.?(pStmt);
}
pub export fn sqlite3_table_column_metadata(db: ?*c.sqlite3, zDbName: [*c]const u8, zTableName: [*c]const u8, zColumnName: [*c]const u8, pzDataType: [*c][*c]const u8, pzCollSeq: [*c][*c]const u8, pNotNull: [*c]c_int, pPrimaryKey: [*c]c_int, pAutoinc: [*c]c_int) c_int {
return sqlite3_api.*.table_column_metadata.?(db, zDbName, zTableName, zColumnName, pzDataType, pzCollSeq, pNotNull, pPrimaryKey, pAutoinc);
}
pub export fn sqlite3_thread_cleanup() void {
return sqlite3_api.*.thread_cleanup.?();
}
pub export fn sqlite3_total_changes(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.total_changes.?(db);
}
pub export fn sqlite3_trace(db: ?*c.sqlite3, xTrace: ?*const fn (?*anyopaque, [*c]const u8) callconv(.c) void, pArg: ?*anyopaque) ?*anyopaque {
return sqlite3_api.*.trace.?(db, xTrace, pArg);
}
pub export fn sqlite3_update_hook(db: ?*c.sqlite3, xCallback: ?*const fn (?*anyopaque, c_int, [*c]const u8, [*c]const u8, c.sqlite3_int64) callconv(.c) void, pArg: ?*anyopaque) ?*anyopaque {
return sqlite3_api.*.update_hook.?(db, xCallback, pArg);
}
pub export fn sqlite3_user_data(pCtx: ?*c.sqlite3_context) ?*anyopaque {
return sqlite3_api.*.user_data.?(pCtx);
}
pub export fn sqlite3_value_blob(pVal: ?*c.sqlite3_value) ?*const anyopaque {
return sqlite3_api.*.value_blob.?(pVal);
}
pub export fn sqlite3_value_bytes(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_bytes.?(pVal);
}
pub export fn sqlite3_value_bytes16(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_bytes16.?(pVal);
}
pub export fn sqlite3_value_double(pVal: ?*c.sqlite3_value) f64 {
return sqlite3_api.*.value_double.?(pVal);
}
pub export fn sqlite3_value_int(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_int.?(pVal);
}
pub export fn sqlite3_value_int64(pVal: ?*c.sqlite3_value) c.sqlite3_int64 {
return sqlite3_api.*.value_int64.?(pVal);
}
pub export fn sqlite3_value_numeric_type(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_numeric_type.?(pVal);
}
pub export fn sqlite3_value_text(pVal: ?*c.sqlite3_value) [*c]const u8 {
return sqlite3_api.*.value_text.?(pVal);
}
pub export fn sqlite3_value_text16(pVal: ?*c.sqlite3_value) ?*const anyopaque {
return sqlite3_api.*.value_text16.?(pVal);
}
pub export fn sqlite3_value_text16be(pVal: ?*c.sqlite3_value) ?*const anyopaque {
return sqlite3_api.*.value_text16be.?(pVal);
}
pub export fn sqlite3_value_text16le(pVal: ?*c.sqlite3_value) ?*const anyopaque {
return sqlite3_api.*.value_text16le.?(pVal);
}
pub export fn sqlite3_value_type(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_type.?(pVal);
}
pub export fn sqlite3_overload_function(db: ?*c.sqlite3, zFuncName: [*c]const u8, nArg: c_int) c_int {
return sqlite3_api.*.overload_function.?(db, zFuncName, nArg);
}
pub export fn sqlite3_clear_bindings(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.clear_bindings.?(pStmt);
}
pub export fn sqlite3_bind_zeroblob(pStmt: ?*c.sqlite3_stmt, i: c_int, n: c_int) c_int {
return sqlite3_api.*.bind_zeroblob.?(pStmt, i, n);
}
pub export fn sqlite3_blob_bytes(pBlob: ?*c.sqlite3_blob) c_int {
return sqlite3_api.*.blob_bytes.?(pBlob);
}
pub export fn sqlite3_blob_close(pBlob: ?*c.sqlite3_blob) c_int {
return sqlite3_api.*.blob_close.?(pBlob);
}
pub export fn sqlite3_blob_open(db: ?*c.sqlite3, zDb: [*c]const u8, zTable: [*c]const u8, zColumn: [*c]const u8, iRow: c.sqlite3_int64, flags: c_int, ppBlob: [*c]?*c.sqlite3_blob) c_int {
return sqlite3_api.*.blob_open.?(db, zDb, zTable, zColumn, iRow, flags, ppBlob);
}
pub export fn sqlite3_blob_read(pBlob: ?*c.sqlite3_blob, z: ?*anyopaque, n: c_int, iOffset: c_int) c_int {
return sqlite3_api.*.blob_read.?(pBlob, z, n, iOffset);
}
pub export fn sqlite3_blob_write(pBlob: ?*c.sqlite3_blob, z: ?*const anyopaque, n: c_int, iOffset: c_int) c_int {
return sqlite3_api.*.blob_write.?(pBlob, z, n, iOffset);
}
pub export fn sqlite3_create_collation_v2(db: ?*c.sqlite3, zName: [*c]const u8, eTextRep: c_int, pCtx: ?*anyopaque, xCompare: ?*const fn (?*anyopaque, c_int, ?*const anyopaque, c_int, ?*const anyopaque) callconv(.c) c_int, xDel: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.create_collation_v2.?(db, zName, eTextRep, pCtx, xCompare, xDel);
}
pub export fn sqlite3_file_control(db: ?*c.sqlite3, zDbName: [*c]const u8, op: c_int, pArg: ?*anyopaque) c_int {
return sqlite3_api.*.file_control.?(db, zDbName, op, pArg);
}
pub export fn sqlite3_memory_highwater(resetFlag: c_int) c.sqlite3_int64 {
return sqlite3_api.*.memory_highwater.?(resetFlag);
}
pub export fn sqlite3_memory_used() c.sqlite3_int64 {
return sqlite3_api.*.memory_used.?();
}
pub export fn sqlite3_mutex_alloc(id: c_int) ?*c.sqlite3_mutex {
return sqlite3_api.*.mutex_alloc.?(id);
}
pub export fn sqlite3_mutex_enter(p: ?*c.sqlite3_mutex) void {
return sqlite3_api.*.mutex_enter.?(p);
}
pub export fn sqlite3_mutex_free(p: ?*c.sqlite3_mutex) void {
return sqlite3_api.*.mutex_free.?(p);
}
pub export fn sqlite3_mutex_leave(p: ?*c.sqlite3_mutex) void {
return sqlite3_api.*.mutex_leave.?(p);
}
pub export fn sqlite3_mutex_try(p: ?*c.sqlite3_mutex) c_int {
return sqlite3_api.*.mutex_try.?(p);
}
pub export fn sqlite3_open_v2(filename: [*c]const u8, ppDb: [*c]?*c.sqlite3, flags: c_int, zVfs: [*c]const u8) c_int {
return sqlite3_api.*.open_v2.?(filename, ppDb, flags, zVfs);
}
pub export fn sqlite3_release_memory(n: c_int) c_int {
return sqlite3_api.*.release_memory.?(n);
}
pub export fn sqlite3_result_error_nomem(pCtx: ?*c.sqlite3_context) void {
return sqlite3_api.*.result_error_nomem.?(pCtx);
}
pub export fn sqlite3_result_error_toobig(pCtx: ?*c.sqlite3_context) void {
return sqlite3_api.*.result_error_toobig.?(pCtx);
}
pub export fn sqlite3_sleep(ms: c_int) c_int {
return sqlite3_api.*.sleep.?(ms);
}
pub export fn sqlite3_soft_heap_limit(n: c_int) void {
return sqlite3_api.*.soft_heap_limit.?(n);
}
pub export fn sqlite3_vfs_find(zVfsName: [*c]const u8) [*c]c.sqlite3_vfs {
return sqlite3_api.*.vfs_find.?(zVfsName);
}
pub export fn sqlite3_vfs_register(pVfs: [*c]c.sqlite3_vfs, makeDflt: c_int) c_int {
return sqlite3_api.*.vfs_register.?(pVfs, makeDflt);
}
pub export fn sqlite3_vfs_unregister(pVfs: [*c]c.sqlite3_vfs) c_int {
return sqlite3_api.*.vfs_unregister.?(pVfs);
}
pub export fn sqlite3_threadsafe() c_int {
return sqlite3_api.*.xthreadsafe.?();
}
pub export fn sqlite3_result_zeroblob(pCtx: ?*c.sqlite3_context, n: c_int) void {
return sqlite3_api.*.result_zeroblob.?(pCtx, n);
}
pub export fn sqlite3_result_error_code(pCtx: ?*c.sqlite3_context, errCode: c_int) void {
return sqlite3_api.*.result_error_code.?(pCtx, errCode);
}
pub export fn sqlite3_randomness(N: c_int, pBuf: ?*anyopaque) void {
return sqlite3_api.*.randomness.?(N, pBuf);
}
pub export fn sqlite3_context_db_handle(pCtx: ?*c.sqlite3_context) ?*c.sqlite3 {
return sqlite3_api.*.context_db_handle.?(pCtx);
}
pub export fn sqlite3_extended_result_codes(pCtx: ?*c.sqlite3, onoff: c_int) c_int {
return sqlite3_api.*.extended_result_codes.?(pCtx, onoff);
}
pub export fn sqlite3_limit(db: ?*c.sqlite3, id: c_int, newVal: c_int) c_int {
return sqlite3_api.*.limit.?(db, id, newVal);
}
pub export fn sqlite3_next_stmt(pDb: ?*c.sqlite3, pStmt: ?*c.sqlite3_stmt) ?*c.sqlite3_stmt {
return sqlite3_api.*.next_stmt.?(pDb, pStmt);
}
pub export fn sqlite3_sql(pStmt: ?*c.sqlite3_stmt) [*c]const u8 {
return sqlite3_api.*.sql.?(pStmt);
}
pub export fn sqlite3_status(op: c_int, pCurrent: [*c]c_int, pHighwater: [*c]c_int, resetFlag: c_int) c_int {
return sqlite3_api.*.status.?(op, pCurrent, pHighwater, resetFlag);
}
pub export fn sqlite3_backup_finish(p: ?*c.sqlite3_backup) c_int {
return sqlite3_api.*.backup_finish.?(p);
}
pub export fn sqlite3_backup_init(pDest: ?*c.sqlite3, zDestName: [*c]const u8, pSource: ?*c.sqlite3, zSourceName: [*c]const u8) ?*c.sqlite3_backup {
return sqlite3_api.*.backup_init.?(pDest, zDestName, pSource, zSourceName);
}
pub export fn sqlite3_backup_pagecount(p: ?*c.sqlite3_backup) c_int {
return sqlite3_api.*.backup_pagecount.?(p);
}
pub export fn sqlite3_backup_remaining(p: ?*c.sqlite3_backup) c_int {
return sqlite3_api.*.backup_remaining.?(p);
}
pub export fn sqlite3_backup_step(p: ?*c.sqlite3_backup, nPage: c_int) c_int {
return sqlite3_api.*.backup_step.?(p, nPage);
}
pub export fn sqlite3_compileoption_get(N: c_int) [*c]const u8 {
return sqlite3_api.*.compileoption_get.?(N);
}
pub export fn sqlite3_compileoption_used(zOptName: [*c]const u8) c_int {
return sqlite3_api.*.compileoption_used.?(zOptName);
}
pub export fn sqlite3_create_function_v2(
db: ?*c.sqlite3,
zFunctionName: [*c]const u8,
nArg: c_int,
eTextRep: c_int,
pApp: ?*anyopaque,
xFunc: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void,
xStep: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void,
xFinal: ?*const fn (?*c.sqlite3_context) callconv(.c) void,
xDestroy: ?*const fn (?*anyopaque) callconv(.c) void,
) c_int {
return sqlite3_api.*.create_function_v2.?(db, zFunctionName, nArg, eTextRep, pApp, xFunc, xStep, xFinal, xDestroy);
}
pub export fn sqlite3_db_mutex(db: ?*c.sqlite3) ?*c.sqlite3_mutex {
return sqlite3_api.*.db_mutex.?(db);
}
pub export fn sqlite3_db_status(db: ?*c.sqlite3, op: c_int, pCurrent: [*c]c_int, pHighwater: [*c]c_int, resetFlag: c_int) c_int {
return sqlite3_api.*.db_status.?(db, op, pCurrent, pHighwater, resetFlag);
}
pub export fn sqlite3_extended_errcode(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.extended_errcode.?(db);
}
pub export fn sqlite3_soft_heap_limit64(N: c.sqlite3_int64) c.sqlite3_int64 {
return sqlite3_api.*.soft_heap_limit64.?(N);
}
pub export fn sqlite3_sourceid() [*c]const u8 {
return sqlite3_api.*.sourceid.?();
}
pub export fn sqlite3_stmt_status(pStmt: ?*c.sqlite3_stmt, op: c_int, resetFlag: c_int) c_int {
return sqlite3_api.*.stmt_status.?(pStmt, op, resetFlag);
}
pub export fn sqlite3_strnicmp(zLeft: [*c]const u8, zRight: [*c]const u8, N: c_int) c_int {
return sqlite3_api.*.strnicmp.?(zLeft, zRight, N);
}
pub export fn sqlite3_unlock_notify(pBlocked: ?*c.sqlite3, xNotify: ?*const fn ([*c]?*anyopaque, c_int) callconv(.c) void, pNotifyArg: ?*anyopaque) c_int {
return sqlite3_api.*.unlock_notify.?(pBlocked, xNotify, pNotifyArg);
}
pub export fn sqlite3_wal_autocheckpoint(db: ?*c.sqlite3, N: c_int) c_int {
return sqlite3_api.*.wal_autocheckpoint.?(db, N);
}
pub export fn sqlite3_wal_checkpoint(db: ?*c.sqlite3, zDb: [*c]const u8) c_int {
return sqlite3_api.*.wal_checkpoint.?(db, zDb);
}
pub export fn sqlite3_wal_hook(db: ?*c.sqlite3, xCallback: ?*const fn (?*anyopaque, ?*c.sqlite3, [*c]const u8, c_int) callconv(.c) c_int, pArg: ?*anyopaque) ?*anyopaque {
return sqlite3_api.*.wal_hook.?(db, xCallback, pArg);
}
pub export fn sqlite3_blob_reopen(pBlob: ?*c.sqlite3_blob, iRow: c.sqlite3_int64) c_int {
return sqlite3_api.*.blob_reopen.?(pBlob, iRow);
}
pub export fn sqlite3_vtab_on_conflict(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.vtab_on_conflict.?(db);
}
pub export fn sqlite3_close_v2(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.close_v2.?(db);
}
pub export fn sqlite3_db_filename(db: ?*c.sqlite3, zDbName: [*c]const u8) [*c]const u8 {
return sqlite3_api.*.db_filename.?(db, zDbName);
}
pub export fn sqlite3_db_readonly(db: ?*c.sqlite3, zDbName: [*c]const u8) c_int {
return sqlite3_api.*.db_readonly.?(db, zDbName);
}
pub export fn sqlite3_db_release_memory(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.db_release_memory.?(db);
}
pub export fn sqlite3_errstr(rc: c_int) [*c]const u8 {
return sqlite3_api.*.errstr.?(rc);
}
pub export fn sqlite3_stmt_busy(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.stmt_busy.?(pStmt);
}
pub export fn sqlite3_stmt_readonly(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.stmt_readonly.?(pStmt);
}
pub export fn sqlite3_stricmp(zLeft: [*c]const u8, zRight: [*c]const u8) c_int {
return sqlite3_api.*.stricmp.?(zLeft, zRight);
}
pub export fn sqlite3_uri_boolean(zFile: [*c]const u8, zParam: [*c]const u8, bDefault: c_int) c_int {
return sqlite3_api.*.uri_boolean.?(zFile, zParam, bDefault);
}
pub export fn sqlite3_uri_int64(zFilename: [*c]const u8, zParam: [*c]const u8, bDflt: c.sqlite3_int64) c.sqlite3_int64 {
return sqlite3_api.*.uri_int64.?(zFilename, zParam, bDflt);
}
pub export fn sqlite3_uri_parameter(zFilename: [*c]const u8, zParam: [*c]const u8) [*c]const u8 {
return sqlite3_api.*.uri_parameter.?(zFilename, zParam);
}
pub export fn sqlite3_wal_checkpoint_v2(db: ?*c.sqlite3, zDb: [*c]const u8, eMode: c_int, pnLog: [*c]c_int, pnCkpt: [*c]c_int) c_int {
return sqlite3_api.*.wal_checkpoint_v2.?(db, zDb, eMode, pnLog, pnCkpt);
}
pub export fn sqlite3_auto_extension(xEntryPoint: ?*const fn () callconv(.c) void) c_int {
return sqlite3_api.*.auto_extension.?(xEntryPoint);
}
pub export fn sqlite3_bind_blob64(pStmt: ?*c.sqlite3_stmt, i: c_int, zData: ?*const anyopaque, nData: c.sqlite3_uint64, xDel: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.bind_blob64.?(pStmt, i, zData, nData, xDel);
}
pub export fn sqlite3_bind_text64(pStmt: ?*c.sqlite3_stmt, i: c_int, zData: [*c]const u8, nData: c.sqlite3_uint64, xDel: ?*const fn (?*anyopaque) callconv(.c) void, encoding: u8) c_int {
return sqlite3_api.*.bind_text64.?(pStmt, i, zData, nData, xDel, encoding);
}
pub export fn sqlite3_cancel_auto_extension(xEntryPoint: ?*const fn () callconv(.c) void) c_int {
return sqlite3_api.*.cancel_auto_extension.?(xEntryPoint);
}
pub export fn sqlite3_load_extension(db: ?*c.sqlite3, zFile: [*c]const u8, zProc: [*c]const u8, pzErrMsg: [*c][*c]u8) c_int {
return sqlite3_api.*.load_extension.?(db, zFile, zProc, pzErrMsg);
}
pub export fn sqlite3_malloc64(n: c.sqlite3_uint64) ?*anyopaque {
return sqlite3_api.*.malloc64.?(n);
}
pub export fn sqlite3_msize(p: ?*anyopaque) c.sqlite3_uint64 {
return sqlite3_api.*.msize.?(p);
}
pub export fn sqlite3_realloc64(pOld: ?*anyopaque, n: c.sqlite3_uint64) ?*anyopaque {
return sqlite3_api.*.realloc64.?(pOld, n);
}
pub export fn sqlite3_reset_auto_extension() void {
return sqlite3_api.*.reset_auto_extension.?();
}
pub export fn sqlite3_result_blob64(pCtx: ?*c.sqlite3_context, z: ?*const anyopaque, n: c.sqlite3_uint64, xDel: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_blob64.?(pCtx, z, n, xDel);
}
pub export fn sqlite3_result_text64(pCtx: ?*c.sqlite3_context, z: [*c]const u8, n: c.sqlite3_uint64, xDel: ?*const fn (?*anyopaque) callconv(.c) void, encoding: u8) void {
return sqlite3_api.*.result_text64.?(pCtx, z, n, xDel, encoding);
}
pub export fn sqlite3_strglob(zGlob: [*c]const u8, zStr: [*c]const u8) c_int {
return sqlite3_api.*.strglob.?(zGlob, zStr);
}
pub export fn sqlite3_value_dup(pOrig: ?*const c.sqlite3_value) ?*c.sqlite3_value {
return sqlite3_api.*.value_dup.?(pOrig);
}
pub export fn sqlite3_value_free(pOld: ?*c.sqlite3_value) void {
return sqlite3_api.*.value_free.?(pOld);
}
pub export fn sqlite3_result_zeroblob64(pCtx: ?*c.sqlite3_context, n: c.sqlite3_uint64) c_int {
return sqlite3_api.*.result_zeroblob64.?(pCtx, n);
}
pub export fn sqlite3_bind_zeroblob64(pStmt: ?*c.sqlite3_stmt, i: c_int, n: c.sqlite3_uint64) c_int {
return sqlite3_api.*.bind_zeroblob64.?(pStmt, i, n);
}
pub export fn sqlite3_value_subtype(pVal: ?*c.sqlite3_value) c_uint {
return sqlite3_api.*.value_subtype.?(pVal);
}
pub export fn sqlite3_result_subtype(pCtx: ?*c.sqlite3_context, eSubtype: c_uint) void {
return sqlite3_api.*.result_subtype.?(pCtx, eSubtype);
}
pub export fn sqlite3_status64(op: c_int, pCurrent: [*c]c.sqlite3_int64, pHighwater: [*c]c.sqlite3_int64, resetFlag: c_int) c_int {
return sqlite3_api.*.status64.?(op, pCurrent, pHighwater, resetFlag);
}
pub export fn sqlite3_strlike(zGlob: [*c]const u8, zStr: [*c]const u8, cEsc: c_uint) c_int {
return sqlite3_api.*.strlike.?(zGlob, zStr, cEsc);
}
pub export fn sqlite3_db_cacheflush(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.db_cacheflush.?(db);
}
pub export fn sqlite3_system_errno(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.system_errno.?(db);
}
pub export fn sqlite3_trace_v2(db: ?*c.sqlite3, uMask: c_uint, xCallback: ?*const fn (c_uint, ?*anyopaque, ?*anyopaque, ?*anyopaque) callconv(.c) c_int, pCtx: ?*anyopaque) c_int {
return sqlite3_api.*.trace_v2.?(db, uMask, xCallback, pCtx);
}
pub export fn sqlite3_expanded_sql(pStmt: ?*c.sqlite3_stmt) [*c]u8 {
return sqlite3_api.*.expanded_sql.?(pStmt);
}
pub export fn sqlite3_set_last_insert_rowid(db: ?*c.sqlite3, iRowid: c.sqlite3_int64) void {
return sqlite3_api.*.set_last_insert_rowid.?(db, iRowid);
}
pub export fn sqlite3_prepare_v3(db: ?*c.sqlite3, zSql: [*c]const u8, nByte: c_int, prepFlags: c_uint, ppStmt: [*c]?*c.sqlite3_stmt, pzTail: [*c][*c]const u8) c_int {
return sqlite3_api.*.prepare_v3.?(db, zSql, nByte, prepFlags, ppStmt, pzTail);
}
pub export fn sqlite3_prepare16_v3(db: ?*c.sqlite3, zSql: ?*const anyopaque, nByte: c_int, prepFlags: c_uint, ppStmt: [*c]?*c.sqlite3_stmt, pzTail: [*c]?*const anyopaque) c_int {
return sqlite3_api.*.prepare16_v3.?(db, zSql, nByte, prepFlags, ppStmt, pzTail);
}
pub export fn sqlite3_bind_pointer(pStmt: ?*c.sqlite3_stmt, i: c_int, pPtr: ?*anyopaque, zPTtype: [*c]const u8, xDestructor: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.bind_pointer.?(pStmt, i, pPtr, zPTtype, xDestructor);
}
pub export fn sqlite3_result_pointer(pCtx: ?*c.sqlite3_context, pPtr: ?*anyopaque, zPType: [*c]const u8, xDestructor: ?*const fn (?*anyopaque) callconv(.c) void) void {
return sqlite3_api.*.result_pointer.?(pCtx, pPtr, zPType, xDestructor);
}
pub export fn sqlite3_value_pointer(pVal: ?*c.sqlite3_value, zPType: [*c]const u8) ?*anyopaque {
return sqlite3_api.*.value_pointer.?(pVal, zPType);
}
pub export fn sqlite3_vtab_nochange(pCtx: ?*c.sqlite3_context) c_int {
return sqlite3_api.*.vtab_nochange.?(pCtx);
}
pub export fn sqlite3_value_nochange(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_nochange.?(pVal);
}
pub export fn sqlite3_vtab_collation(pIdxInfo: [*c]c.sqlite3_index_info, iCons: c_int) [*c]const u8 {
return sqlite3_api.*.vtab_collation.?(pIdxInfo, iCons);
}
pub export fn sqlite3_keyword_count() c_int {
return sqlite3_api.*.keyword_count.?();
}
pub export fn sqlite3_keyword_name(i: c_int, pzName: [*c][*c]const u8, pnName: [*c]c_int) c_int {
return sqlite3_api.*.keyword_name.?(i, pzName, pnName);
}
pub export fn sqlite3_keyword_check(zName: [*c]const u8, nName: c_int) c_int {
return sqlite3_api.*.keyword_check.?(zName, nName);
}
pub export fn sqlite3_str_new(db: ?*c.sqlite3) ?*c.sqlite3_str {
return sqlite3_api.*.str_new.?(db);
}
pub export fn sqlite3_str_finish(p: ?*c.sqlite3_str) [*c]u8 {
return sqlite3_api.*.str_finish.?(p);
}
pub export fn sqlite3_str_append(p: ?*c.sqlite3_str, zIn: [*c]const u8, N: c_int) void {
return sqlite3_api.*.str_append.?(p, zIn, N);
}
pub export fn sqlite3_str_appendall(p: ?*c.sqlite3_str, zIn: [*c]const u8) void {
return sqlite3_api.*.str_appendall.?(p, zIn);
}
pub export fn sqlite3_str_appendchar(p: ?*c.sqlite3_str, N: c_int, C: u8) void {
return sqlite3_api.*.str_appendchar.?(p, N, C);
}
pub export fn sqlite3_str_reset(p: ?*c.sqlite3_str) void {
return sqlite3_api.*.str_reset.?(p);
}
pub export fn sqlite3_str_errcode(p: ?*c.sqlite3_str) c_int {
return sqlite3_api.*.str_errcode.?(p);
}
pub export fn sqlite3_str_length(p: ?*c.sqlite3_str) c_int {
return sqlite3_api.*.str_length.?(p);
}
pub export fn sqlite3_str_value(p: ?*c.sqlite3_str) [*c]u8 {
return sqlite3_api.*.str_value.?(p);
}
pub export fn sqlite3_create_window_function(
db: ?*c.sqlite3,
zFunctionName: [*c]const u8,
nArg: c_int,
eTextRep: c_int,
pArg: ?*anyopaque,
xStep: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void,
xFinal: ?*const fn (?*c.sqlite3_context) callconv(.c) void,
xValue: ?*const fn (?*c.sqlite3_context) callconv(.c) void,
xInverse: ?*const fn (?*c.sqlite3_context, c_int, [*c]?*c.sqlite3_value) callconv(.c) void,
xDestroy: ?*const fn (?*anyopaque) callconv(.c) void,
) c_int {
return sqlite3_api.*.create_window_function.?(
db,
zFunctionName,
nArg,
eTextRep,
pArg,
xStep,
xFinal,
xValue,
xInverse,
xDestroy,
);
}
pub export fn sqlite3_stmt_isexplain(pStmt: ?*c.sqlite3_stmt) c_int {
return sqlite3_api.*.stmt_isexplain.?(pStmt);
}
pub export fn sqlite3_value_frombind(pVal: ?*c.sqlite3_value) c_int {
return sqlite3_api.*.value_frombind.?(pVal);
}
pub export fn sqlite3_drop_modules(db: ?*c.sqlite3, azKeep: [*c][*c]const u8) c_int {
return sqlite3_api.*.drop_modules.?(db, azKeep);
}
pub export fn sqlite3_hard_heap_limit64(N: c.sqlite3_int64) c.sqlite3_int64 {
return sqlite3_api.*.hard_heap_limit64.?(N);
}
pub export fn sqlite3_uri_key(zFilename: [*c]const u8, N: c_int) [*c]const u8 {
return sqlite3_api.*.uri_key.?(zFilename, N);
}
pub export fn sqlite3_filename_database(zFilename: [*c]const u8) [*c]const u8 {
return sqlite3_api.*.filename_database.?(zFilename);
}
pub export fn sqlite3_filename_journal(zFilename: [*c]const u8) [*c]const u8 {
return sqlite3_api.*.filename_journal.?(zFilename);
}
pub export fn sqlite3_filename_wal(zFilename: [*c]const u8) [*c]const u8 {
return sqlite3_api.*.filename_wal.?(zFilename);
}
pub export fn sqlite3_create_filename(zDatabase: [*c]const u8, zJournal: [*c]const u8, zWal: [*c]const u8, nParam: c_int, azParam: [*c][*c]const u8) [*c]const u8 {
return sqlite3_api.*.create_filename.?(zDatabase, zJournal, zWal, nParam, azParam);
}
pub export fn sqlite3_free_filename(p: [*c]u8) void {
return sqlite3_api.*.free_filename.?(p);
}
pub export fn sqlite3_database_file_object(zName: [*c]const u8) [*c]c.sqlite3_file {
return sqlite3_api.*.database_file_object.?(zName);
}
pub export fn sqlite3_txn_state(db: ?*c.sqlite3, zSchema: [*c]const u8) c_int {
return sqlite3_api.*.txn_state.?(db, zSchema);
}
pub export fn sqlite3_changes64(db: ?*c.sqlite3) c.sqlite3_int64 {
return sqlite3_api.*.changes64.?(db);
}
pub export fn sqlite3_total_changes64(db: ?*c.sqlite3) c.sqlite3_int64 {
return sqlite3_api.*.total_changes64.?(db);
}
pub export fn sqlite3_autovacuum_pages(db: ?*c.sqlite3, xCallback: ?*const fn (?*anyopaque, [*c]const u8, c_uint, c_uint, c_uint) callconv(.c) c_uint, pArg: ?*anyopaque, xDestructor: ?*const fn (?*anyopaque) callconv(.c) void) c_int {
return sqlite3_api.*.autovacuum_pages.?(db, xCallback, pArg, xDestructor);
}
pub export fn sqlite3_error_offset(db: ?*c.sqlite3) c_int {
return sqlite3_api.*.error_offset.?(db);
}
pub export fn sqlite3_vtab_rhs_value(pIdxInfo: [*c]c.sqlite3_index_info, iCons: c_int, ppVal: [*c]?*c.sqlite3_value) c_int {
return sqlite3_api.*.vtab_rhs_value.?(pIdxInfo, iCons, ppVal);
}
pub export fn sqlite3_vtab_distinct(pIdxInfo: [*c]c.sqlite3_index_info) c_int {
return sqlite3_api.*.vtab_distinct.?(pIdxInfo);
}
pub export fn sqlite3_vtab_in(pIdxInfo: [*c]c.sqlite3_index_info, iCons: c_int, bHandle: c_int) c_int {
return sqlite3_api.*.vtab_in.?(pIdxInfo, iCons, bHandle);
}
pub export fn sqlite3_vtab_in_first(pVal: ?*c.sqlite3_value, ppOut: [*c]?*c.sqlite3_value) c_int {
return sqlite3_api.*.vtab_in_first.?(pVal, ppOut);
}
pub export fn sqlite3_vtab_in_next(pVal: ?*c.sqlite3_value, ppOut: [*c]?*c.sqlite3_value) c_int {
return sqlite3_api.*.vtab_in_next.?(pVal, ppOut);
}
pub export fn sqlite3_deserialize(db: ?*c.sqlite3, zSchema: [*c]const u8, pData: [*c]u8, szDb: c.sqlite3_int64, szBuf: c.sqlite3_int64, mFlags: c_uint) c_int {
return sqlite3_api.*.deserialize.?(db, zSchema, pData, szDb, szBuf, mFlags);
}
pub export fn sqlite3_serialize(db: ?*c.sqlite3, zSchema: [*c]const u8, piSize: [*c]c.sqlite3_int64, mFlags: c_uint) [*c]u8 {
return sqlite3_api.*.serialize.?(db, zSchema, piSize, mFlags);
}
pub export fn sqlite3_db_name(db: ?*c.sqlite3, N: c_int) [*c]const u8 {
return sqlite3_api.*.db_name.?(db, N);
}

View File

@@ -0,0 +1,5 @@
#include "workaround.h"
my_sqlite3_destructor_type sqliteTransientAsDestructor() {
return (my_sqlite3_destructor_type)-1;
}

View File

@@ -0,0 +1,3 @@
typedef void (*my_sqlite3_destructor_type)(void *);
my_sqlite3_destructor_type sqliteTransientAsDestructor();

View File

@@ -0,0 +1,311 @@
const std = @import("std");
const mem = std.mem;
const c = @import("c.zig").c;
const versionGreaterThanOrEqualTo = @import("c.zig").versionGreaterThanOrEqualTo;
pub const SQLiteExtendedIOError = error{
SQLiteIOErrRead,
SQLiteIOErrShortRead,
SQLiteIOErrWrite,
SQLiteIOErrFsync,
SQLiteIOErrDirFsync,
SQLiteIOErrTruncate,
SQLiteIOErrFstat,
SQLiteIOErrUnlock,
SQLiteIOErrRDLock,
SQLiteIOErrDelete,
SQLiteIOErrBlocked,
SQLiteIOErrNoMem,
SQLiteIOErrAccess,
SQLiteIOErrCheckReservedLock,
SQLiteIOErrLock,
SQLiteIOErrClose,
SQLiteIOErrDirClose,
SQLiteIOErrSHMOpen,
SQLiteIOErrSHMSize,
SQLiteIOErrSHMLock,
SQLiteIOErrSHMMap,
SQLiteIOErrSeek,
SQLiteIOErrDeleteNoEnt,
SQLiteIOErrMmap,
SQLiteIOErrGetTempPath,
SQLiteIOErrConvPath,
SQLiteIOErrVnode,
SQLiteIOErrAuth,
SQLiteIOErrBeginAtomic,
SQLiteIOErrCommitAtomic,
SQLiteIOErrRollbackAtomic,
SQLiteIOErrData,
SQLiteIOErrCorruptFS,
};
pub const SQLiteExtendedCantOpenError = error{
SQLiteCantOpenNoTempDir,
SQLiteCantOpenIsDir,
SQLiteCantOpenFullPath,
SQLiteCantOpenConvPath,
SQLiteCantOpenDirtyWAL,
SQLiteCantOpenSymlink,
};
pub const SQLiteExtendedReadOnlyError = error{
SQLiteReadOnlyRecovery,
SQLiteReadOnlyCantLock,
SQLiteReadOnlyRollback,
SQLiteReadOnlyDBMoved,
SQLiteReadOnlyCantInit,
SQLiteReadOnlyDirectory,
};
pub const SQLiteExtendedConstraintError = error{
SQLiteConstraintCheck,
SQLiteConstraintCommitHook,
SQLiteConstraintForeignKey,
SQLiteConstraintFunction,
SQLiteConstraintNotNull,
SQLiteConstraintPrimaryKey,
SQLiteConstraintTrigger,
SQLiteConstraintUnique,
SQLiteConstraintVTab,
SQLiteConstraintRowID,
SQLiteConstraintPinned,
};
pub const SQLiteExtendedError = error{
SQLiteErrorMissingCollSeq,
SQLiteErrorRetry,
SQLiteErrorSnapshot,
SQLiteLockedSharedCache,
SQLiteLockedVTab,
SQLiteBusyRecovery,
SQLiteBusySnapshot,
SQLiteBusyTimeout,
SQLiteCorruptVTab,
SQLiteCorruptSequence,
SQLiteCorruptIndex,
SQLiteAbortRollback,
};
pub const SQLiteError = error{
SQLiteError,
SQLiteInternal,
SQLitePerm,
SQLiteAbort,
SQLiteBusy,
SQLiteLocked,
SQLiteNoMem,
SQLiteReadOnly,
SQLiteInterrupt,
SQLiteIOErr,
SQLiteCorrupt,
SQLiteNotFound,
SQLiteFull,
SQLiteCantOpen,
SQLiteProtocol,
SQLiteEmpty,
SQLiteSchema,
SQLiteTooBig,
SQLiteConstraint,
SQLiteMismatch,
SQLiteMisuse,
SQLiteNoLFS,
SQLiteAuth,
SQLiteRange,
SQLiteNotADatabase,
SQLiteNotice,
SQLiteWarning,
};
pub const Error = SQLiteError ||
SQLiteExtendedError ||
SQLiteExtendedIOError ||
SQLiteExtendedCantOpenError ||
SQLiteExtendedReadOnlyError ||
SQLiteExtendedConstraintError;
pub fn errorFromResultCode(code: c_int) Error {
// These errors are only available since 3.22.0.
if (comptime versionGreaterThanOrEqualTo(3, 22, 0)) {
switch (code) {
c.SQLITE_ERROR_MISSING_COLLSEQ => return error.SQLiteErrorMissingCollSeq,
c.SQLITE_ERROR_RETRY => return error.SQLiteErrorRetry,
c.SQLITE_READONLY_CANTINIT => return error.SQLiteReadOnlyCantInit,
c.SQLITE_READONLY_DIRECTORY => return error.SQLiteReadOnlyDirectory,
else => {},
}
}
// These errors are only available since 3.25.0.
if (comptime versionGreaterThanOrEqualTo(3, 25, 0)) {
switch (code) {
c.SQLITE_ERROR_SNAPSHOT => return error.SQLiteErrorSnapshot,
c.SQLITE_LOCKED_VTAB => return error.SQLiteLockedVTab,
c.SQLITE_CANTOPEN_DIRTYWAL => return error.SQLiteCantOpenDirtyWAL,
c.SQLITE_CORRUPT_SEQUENCE => return error.SQLiteCorruptSequence,
else => {},
}
}
// These errors are only available since 3.31.0.
if (comptime versionGreaterThanOrEqualTo(3, 31, 0)) {
switch (code) {
c.SQLITE_CANTOPEN_SYMLINK => return error.SQLiteCantOpenSymlink,
c.SQLITE_CONSTRAINT_PINNED => return error.SQLiteConstraintPinned,
else => {},
}
}
// These errors are only available since 3.32.0.
if (comptime versionGreaterThanOrEqualTo(3, 32, 0)) {
switch (code) {
c.SQLITE_IOERR_DATA => return error.SQLiteIOErrData, // See https://sqlite.org/cksumvfs.html
c.SQLITE_BUSY_TIMEOUT => return error.SQLiteBusyTimeout,
c.SQLITE_CORRUPT_INDEX => return error.SQLiteCorruptIndex,
else => {},
}
}
// These errors are only available since 3.34.0.
if (comptime versionGreaterThanOrEqualTo(3, 34, 0)) {
switch (code) {
c.SQLITE_IOERR_CORRUPTFS => return error.SQLiteIOErrCorruptFS,
else => {},
}
}
switch (code) {
c.SQLITE_ERROR => return error.SQLiteError,
c.SQLITE_INTERNAL => return error.SQLiteInternal,
c.SQLITE_PERM => return error.SQLitePerm,
c.SQLITE_ABORT => return error.SQLiteAbort,
c.SQLITE_BUSY => return error.SQLiteBusy,
c.SQLITE_LOCKED => return error.SQLiteLocked,
c.SQLITE_NOMEM => return error.SQLiteNoMem,
c.SQLITE_READONLY => return error.SQLiteReadOnly,
c.SQLITE_INTERRUPT => return error.SQLiteInterrupt,
c.SQLITE_IOERR => return error.SQLiteIOErr,
c.SQLITE_CORRUPT => return error.SQLiteCorrupt,
c.SQLITE_NOTFOUND => return error.SQLiteNotFound,
c.SQLITE_FULL => return error.SQLiteFull,
c.SQLITE_CANTOPEN => return error.SQLiteCantOpen,
c.SQLITE_PROTOCOL => return error.SQLiteProtocol,
c.SQLITE_EMPTY => return error.SQLiteEmpty,
c.SQLITE_SCHEMA => return error.SQLiteSchema,
c.SQLITE_TOOBIG => return error.SQLiteTooBig,
c.SQLITE_CONSTRAINT => return error.SQLiteConstraint,
c.SQLITE_MISMATCH => return error.SQLiteMismatch,
c.SQLITE_MISUSE => return error.SQLiteMisuse,
c.SQLITE_NOLFS => return error.SQLiteNoLFS,
c.SQLITE_AUTH => return error.SQLiteAuth,
c.SQLITE_RANGE => return error.SQLiteRange,
c.SQLITE_NOTADB => return error.SQLiteNotADatabase,
c.SQLITE_NOTICE => return error.SQLiteNotice,
c.SQLITE_WARNING => return error.SQLiteWarning,
c.SQLITE_IOERR_READ => return error.SQLiteIOErrRead,
c.SQLITE_IOERR_SHORT_READ => return error.SQLiteIOErrShortRead,
c.SQLITE_IOERR_WRITE => return error.SQLiteIOErrWrite,
c.SQLITE_IOERR_FSYNC => return error.SQLiteIOErrFsync,
c.SQLITE_IOERR_DIR_FSYNC => return error.SQLiteIOErrDirFsync,
c.SQLITE_IOERR_TRUNCATE => return error.SQLiteIOErrTruncate,
c.SQLITE_IOERR_FSTAT => return error.SQLiteIOErrFstat,
c.SQLITE_IOERR_UNLOCK => return error.SQLiteIOErrUnlock,
c.SQLITE_IOERR_RDLOCK => return error.SQLiteIOErrRDLock,
c.SQLITE_IOERR_DELETE => return error.SQLiteIOErrDelete,
c.SQLITE_IOERR_BLOCKED => return error.SQLiteIOErrBlocked,
c.SQLITE_IOERR_NOMEM => return error.SQLiteIOErrNoMem,
c.SQLITE_IOERR_ACCESS => return error.SQLiteIOErrAccess,
c.SQLITE_IOERR_CHECKRESERVEDLOCK => return error.SQLiteIOErrCheckReservedLock,
c.SQLITE_IOERR_LOCK => return error.SQLiteIOErrLock,
c.SQLITE_IOERR_CLOSE => return error.SQLiteIOErrClose,
c.SQLITE_IOERR_DIR_CLOSE => return error.SQLiteIOErrDirClose,
c.SQLITE_IOERR_SHMOPEN => return error.SQLiteIOErrSHMOpen,
c.SQLITE_IOERR_SHMSIZE => return error.SQLiteIOErrSHMSize,
c.SQLITE_IOERR_SHMLOCK => return error.SQLiteIOErrSHMLock,
c.SQLITE_IOERR_SHMMAP => return error.SQLiteIOErrSHMMap,
c.SQLITE_IOERR_SEEK => return error.SQLiteIOErrSeek,
c.SQLITE_IOERR_DELETE_NOENT => return error.SQLiteIOErrDeleteNoEnt,
c.SQLITE_IOERR_MMAP => return error.SQLiteIOErrMmap,
c.SQLITE_IOERR_GETTEMPPATH => return error.SQLiteIOErrGetTempPath,
c.SQLITE_IOERR_CONVPATH => return error.SQLiteIOErrConvPath,
c.SQLITE_IOERR_VNODE => return error.SQLiteIOErrVnode,
c.SQLITE_IOERR_AUTH => return error.SQLiteIOErrAuth,
c.SQLITE_IOERR_BEGIN_ATOMIC => return error.SQLiteIOErrBeginAtomic,
c.SQLITE_IOERR_COMMIT_ATOMIC => return error.SQLiteIOErrCommitAtomic,
c.SQLITE_IOERR_ROLLBACK_ATOMIC => return error.SQLiteIOErrRollbackAtomic,
c.SQLITE_LOCKED_SHAREDCACHE => return error.SQLiteLockedSharedCache,
c.SQLITE_BUSY_RECOVERY => return error.SQLiteBusyRecovery,
c.SQLITE_BUSY_SNAPSHOT => return error.SQLiteBusySnapshot,
c.SQLITE_CANTOPEN_NOTEMPDIR => return error.SQLiteCantOpenNoTempDir,
c.SQLITE_CANTOPEN_ISDIR => return error.SQLiteCantOpenIsDir,
c.SQLITE_CANTOPEN_FULLPATH => return error.SQLiteCantOpenFullPath,
c.SQLITE_CANTOPEN_CONVPATH => return error.SQLiteCantOpenConvPath,
c.SQLITE_CORRUPT_VTAB => return error.SQLiteCorruptVTab,
c.SQLITE_READONLY_RECOVERY => return error.SQLiteReadOnlyRecovery,
c.SQLITE_READONLY_CANTLOCK => return error.SQLiteReadOnlyCantLock,
c.SQLITE_READONLY_ROLLBACK => return error.SQLiteReadOnlyRollback,
c.SQLITE_READONLY_DBMOVED => return error.SQLiteReadOnlyDBMoved,
c.SQLITE_ABORT_ROLLBACK => return error.SQLiteAbortRollback,
c.SQLITE_CONSTRAINT_CHECK => return error.SQLiteConstraintCheck,
c.SQLITE_CONSTRAINT_COMMITHOOK => return error.SQLiteConstraintCommitHook,
c.SQLITE_CONSTRAINT_FOREIGNKEY => return error.SQLiteConstraintForeignKey,
c.SQLITE_CONSTRAINT_FUNCTION => return error.SQLiteConstraintFunction,
c.SQLITE_CONSTRAINT_NOTNULL => return error.SQLiteConstraintNotNull,
c.SQLITE_CONSTRAINT_PRIMARYKEY => return error.SQLiteConstraintPrimaryKey,
c.SQLITE_CONSTRAINT_TRIGGER => return error.SQLiteConstraintTrigger,
c.SQLITE_CONSTRAINT_UNIQUE => return error.SQLiteConstraintUnique,
c.SQLITE_CONSTRAINT_VTAB => return error.SQLiteConstraintVTab,
c.SQLITE_CONSTRAINT_ROWID => return error.SQLiteConstraintRowID,
else => std.debug.panic("invalid result code {}", .{code}),
}
}
/// DetailedError contains a SQLite error code and error message.
pub const DetailedError = struct {
code: usize,
near: i32,
message: []const u8,
pub fn format(self: @This(), writer: anytype) !void {
_ = try writer.print("{{code: {}, near: {d}, message: {s}}}", .{ self.code, self.near, self.message });
}
};
pub fn getDetailedErrorFromResultCode(code: c_int) DetailedError {
return .{
.code = @intCast(code),
.near = -1,
.message = blk: {
const msg = c.sqlite3_errstr(code);
break :blk mem.sliceTo(msg, 0);
},
};
}
pub fn getErrorOffset(db: *c.sqlite3) i32 {
if (comptime versionGreaterThanOrEqualTo(3, 38, 0)) {
return c.sqlite3_error_offset(db);
}
return -1;
}
pub fn getLastDetailedErrorFromDb(db: *c.sqlite3) DetailedError {
return .{
.code = @intCast(c.sqlite3_extended_errcode(db)),
.near = getErrorOffset(db),
.message = blk: {
const msg = c.sqlite3_errmsg(db);
break :blk mem.sliceTo(msg, 0);
},
};
}

View File

@@ -0,0 +1,59 @@
const std = @import("std");
const Blake3 = std.crypto.hash.Blake3;
const Sha3_512 = std.crypto.hash.sha3.Sha3_512;
const sqlite = @import("sqlite");
const c = sqlite.c;
const name = "zigcrypto";
pub const loadable_extension = true;
var module_allocator: std.heap.GeneralPurposeAllocator(.{}) = undefined;
var module_context: sqlite.vtab.ModuleContext = undefined;
const logger = std.log.scoped(.zigcrypto);
fn createAllFunctions(db: *sqlite.Db) !void {
try db.createScalarFunction(
"blake3",
struct {
fn run(input: []const u8) [Blake3.digest_length]u8 {
var output: [Blake3.digest_length]u8 = undefined;
Blake3.hash(input, output[0..], .{});
return output;
}
}.run,
.{},
);
try db.createScalarFunction(
"sha3_512",
struct {
fn run(input: []const u8) [Sha3_512.digest_length]u8 {
var output: [Sha3_512.digest_length]u8 = undefined;
Sha3_512.hash(input, output[0..], .{});
return output;
}
}.run,
.{},
);
}
pub export fn sqlite3_zigcrypto_init(raw_db: *c.sqlite3, err_msg: [*c][*c]u8, api: *c.sqlite3_api_routines) callconv(.c) c_int {
_ = err_msg;
c.sqlite3_api = api;
module_allocator = std.heap.GeneralPurposeAllocator(.{}){};
var db = sqlite.Db{
.db = raw_db,
};
createAllFunctions(&db) catch |err| {
logger.err("unable to create all SQLite functions, err: {!}", .{err});
return c.SQLITE_ERROR;
};
return c.SQLITE_OK;
}

View File

@@ -0,0 +1,56 @@
const builtin = @import("builtin");
const std = @import("std");
const debug = std.debug;
const mem = std.mem;
const sqlite = @import("sqlite");
pub fn main() anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const allocator = arena.allocator();
//
var db = try sqlite.Db.init(.{
.mode = sqlite.Db.Mode{ .Memory = {} },
.open_flags = .{ .write = true },
});
defer db.deinit();
{
const result = sqlite.c.sqlite3_enable_load_extension(db.db, 1);
debug.assert(result == sqlite.c.SQLITE_OK);
}
{
const extension_path = if (builtin.os.tag == .windows)
"./zig-out/bin/zigcrypto.dll"
else
"./zig-out/lib/libzigcrypto";
var pzErrMsg: [*c]u8 = undefined;
const result = sqlite.c.sqlite3_load_extension(db.db, extension_path, null, &pzErrMsg);
if (result != sqlite.c.SQLITE_OK) {
const err = sqlite.c.sqlite3_errstr(result);
std.debug.panic("unable to load extension at path {s}, err: {s}, err message: {s}\n", .{ extension_path, err, std.mem.sliceTo(pzErrMsg, 0) });
}
}
var diags = sqlite.Diagnostics{};
const blake3_digest = db.oneAlloc([]const u8, allocator, "SELECT hex(blake3('foobar'))", .{ .diags = &diags }, .{}) catch |err| {
debug.print("unable to get blake3 hash, err: {}, diags: {f}\n", .{ err, diags });
return err;
};
debug.assert(blake3_digest != null);
debug.assert(mem.eql(u8, "AA51DCD43D5C6C5203EE16906FD6B35DB298B9B2E1DE3FCE81811D4806B76B7D", blake3_digest.?));
const sha3_digest = db.oneAlloc([]const u8, allocator, "SELECT hex(sha3_512('foobar'))", .{ .diags = &diags }, .{}) catch |err| {
debug.print("unable to get sha3 hash, err: {}, diags: {f}\n", .{ err, diags });
return err;
};
debug.assert(sha3_digest != null);
debug.assert(mem.eql(u8, "FF32A30C3AF5012EA395827A3E99A13073C3A8D8410A708568FF7E6EB85968FCCFEBAEA039BC21411E9D43FDB9A851B529B9960FFEA8679199781B8F45CA85E2", sha3_digest.?));
}

View File

@@ -0,0 +1,104 @@
const std = @import("std");
const debug = std.debug;
const c = @import("c.zig").c;
const Blob = @import("sqlite.zig").Blob;
const Text = @import("sqlite.zig").Text;
/// Sets the result of a function call in the context `ctx`.
///
/// Determines at compile time which sqlite3_result_XYZ function to use based on the type of `result`.
pub fn setResult(ctx: ?*c.sqlite3_context, result: anytype) void {
const ResultType = @TypeOf(result);
switch (ResultType) {
Text => c.sqlite3_result_text(ctx, result.data.ptr, @intCast(result.data.len), c.sqliteTransientAsDestructor()),
Blob => c.sqlite3_result_blob(ctx, result.data.ptr, @intCast(result.data.len), c.sqliteTransientAsDestructor()),
else => switch (@typeInfo(ResultType)) {
.int => |info| if ((info.bits + if (info.signedness == .unsigned) 1 else 0) <= 32) {
c.sqlite3_result_int(ctx, result);
} else if ((info.bits + if (info.signedness == .unsigned) 1 else 0) <= 64) {
c.sqlite3_result_int64(ctx, result);
} else {
@compileError("integer " ++ @typeName(ResultType) ++ " is not representable in sqlite");
},
.float => c.sqlite3_result_double(ctx, result),
.bool => c.sqlite3_result_int(ctx, if (result) 1 else 0),
.array => |arr| switch (arr.child) {
u8 => c.sqlite3_result_blob(ctx, &result, arr.len, c.sqliteTransientAsDestructor()),
else => @compileError("cannot use a result of type " ++ @typeName(ResultType)),
},
.pointer => |ptr| switch (ptr.size) {
.slice => switch (ptr.child) {
u8 => c.sqlite3_result_text(ctx, result.ptr, @intCast(result.len), c.sqliteTransientAsDestructor()),
else => @compileError("cannot use a result of type " ++ @typeName(ResultType)),
},
else => @compileError("cannot use a result of type " ++ @typeName(ResultType)),
},
else => @compileError("cannot use a result of type " ++ @typeName(ResultType)),
},
}
}
/// Sets a type using the provided value.
///
/// Determines at compile time which sqlite3_value_XYZ function to use based on the type `ArgType`.
pub fn setTypeFromValue(comptime ArgType: type, arg: *ArgType, sqlite_value: *c.sqlite3_value) void {
switch (ArgType) {
Text => arg.*.data = sliceFromValue(sqlite_value),
Blob => arg.*.data = sliceFromValue(sqlite_value),
else => switch (@typeInfo(ArgType)) {
.int => |info| if ((info.bits + if (info.signedness == .unsigned) 1 else 0) <= 32) {
const value = c.sqlite3_value_int(sqlite_value);
arg.* = @intCast(value);
} else if ((info.bits + if (info.signedness == .unsigned) 1 else 0) <= 64) {
const value = c.sqlite3_value_int64(sqlite_value);
arg.* = @intCast(value);
} else {
@compileError("integer " ++ @typeName(ArgType) ++ " is not representable in sqlite");
},
.float => {
const value = c.sqlite3_value_double(sqlite_value);
arg.* = @floatCast(value);
},
.bool => {
const value = c.sqlite3_value_int(sqlite_value);
arg.* = value > 0;
},
.pointer => |ptr| switch (ptr.size) {
.slice => switch (ptr.child) {
u8 => arg.* = sliceFromValue(sqlite_value),
else => @compileError("cannot use an argument of type " ++ @typeName(ArgType)),
},
else => @compileError("cannot use an argument of type " ++ @typeName(ArgType)),
},
else => @compileError("cannot use an argument of type " ++ @typeName(ArgType)),
},
}
}
fn sliceFromValue(sqlite_value: *c.sqlite3_value) []const u8 {
const size: usize = @intCast(c.sqlite3_value_bytes(sqlite_value));
const value = c.sqlite3_value_text(sqlite_value);
debug.assert(value != null); // TODO(vincent): how do we handle this properly ?
return value[0..size];
}
// Returns true if the type T has a function named `name`.
pub fn hasFn(comptime T: type, comptime name: []const u8) bool {
if (!@hasDecl(T, name)) {
return false;
}
const decl = @field(T, name);
const decl_type = @TypeOf(decl);
const decl_type_info = @typeInfo(decl_type);
return switch (decl_type_info) {
.@"fn" => true,
else => false,
};
}

View File

@@ -0,0 +1,2 @@
[tools]
zig = "master"

View File

@@ -0,0 +1,465 @@
const std = @import("std");
const mem = std.mem;
const testing = std.testing;
const Blob = @import("sqlite.zig").Blob;
const Text = @import("sqlite.zig").Text;
const BindMarker = struct {
/// Name of the bind parameter in case it's named.
name: ?[]const u8 = null,
/// Contains the expected type for a bind parameter which will be checked
/// at comptime when calling bind on a statement.
///
/// A null means the bind parameter is untyped so there won't be comptime checking.
typed: ?type = null,
};
fn isNamedIdentifierChar(c: u8) bool {
return std.ascii.isAlphabetic(c) or std.ascii.isDigit(c) or c == '_';
}
fn bindMarkerForName(comptime markers: []const BindMarker, comptime name: []const u8) ?BindMarker {
for (markers) |marker| {
if (marker.name != null and std.mem.eql(u8, marker.name.?, name))
return marker;
}
return null;
}
pub fn ParsedQuery(comptime tmp_query: []const u8) type {
return struct {
const Self = @This();
const result = parse();
pub const bind_markers = result.bind_markers[0..result.bind_markers_len];
pub fn getQuery() []const u8 {
return Self.result.query[0..Self.result.query_len];
}
const ParsedQueryResult = struct {
bind_markers: [128]BindMarker,
bind_markers_len: usize,
query: [tmp_query.len]u8,
query_len: usize,
};
fn parse() ParsedQueryResult {
// This contains the final SQL query after parsing with our
// own typed bind markers removed.
var buf: [tmp_query.len]u8 = undefined;
var pos = 0;
var state = .start;
// This holds the starting character of the string while
// state is .inside_string so that we know which type of
// string we're exiting from
var string_starting_character: u8 = undefined;
var current_bind_marker_type: [256]u8 = undefined;
var current_bind_marker_type_pos = 0;
// becomes part of our result
var tmp_bind_markers: [128]BindMarker = undefined;
var nb_tmp_bind_markers: usize = 0;
// used for capturing slices, such as bind parameter name
var hold_pos = 0;
for (tmp_query) |c| {
switch (state) {
.start => switch (c) {
'?', ':', '@', '$' => {
tmp_bind_markers[nb_tmp_bind_markers] = BindMarker{};
current_bind_marker_type_pos = 0;
state = .bind_marker;
buf[pos] = c;
pos += 1;
},
'\'', '"', '[', '`' => {
state = .inside_string;
string_starting_character = c;
buf[pos] = c;
pos += 1;
},
else => {
buf[pos] = c;
pos += 1;
},
},
.inside_string => switch (c) {
'\'' => {
if (string_starting_character == '\'') state = .start;
buf[pos] = c;
pos += 1;
},
'"' => {
if (string_starting_character == '"') state = .start;
buf[pos] = c;
pos += 1;
},
']' => {
if (string_starting_character == '[') state = .start;
buf[pos] = c;
pos += 1;
},
'`' => {
if (string_starting_character == '`') state = .start;
buf[pos] = c;
pos += 1;
},
else => {
buf[pos] = c;
pos += 1;
},
},
.bind_marker => switch (c) {
'?', ':', '@', '$' => @compileError("invalid multiple '?', ':', '$' or '@'."),
'{' => {
state = .bind_marker_type;
},
else => {
if (isNamedIdentifierChar(c)) {
// This is the start of a named bind marker.
state = .bind_marker_identifier;
hold_pos = pos + 1;
} else {
// This is a unnamed, untyped bind marker.
state = .start;
tmp_bind_markers[nb_tmp_bind_markers].typed = null;
nb_tmp_bind_markers += 1;
}
buf[pos] = c;
pos += 1;
},
},
.bind_marker_identifier => switch (c) {
'?', ':', '@', '$' => @compileError("unregconised multiple '?', ':', '$' or '@'."),
'{' => {
state = .bind_marker_type;
current_bind_marker_type_pos = 0;
},
else => {
if (!isNamedIdentifierChar(c)) {
// This marks the end of the named bind marker.
state = .start;
const name = buf[hold_pos - 1 .. pos];
// TODO(vincent): name retains a pointer to a comptime var, FIX !
if (bindMarkerForName(tmp_bind_markers[0..nb_tmp_bind_markers], name) == null) {
const new_buf = buf;
tmp_bind_markers[nb_tmp_bind_markers].name = new_buf[hold_pos - 1 .. pos];
nb_tmp_bind_markers += 1;
}
}
buf[pos] = c;
pos += 1;
},
},
.bind_marker_type => switch (c) {
'}' => {
state = .start;
const type_info_string = current_bind_marker_type[0..current_bind_marker_type_pos];
// Handles optional types
const typ = if (type_info_string[0] == '?') blk: {
const child_type = ParseType(type_info_string[1..]);
break :blk ?child_type;
} else blk: {
break :blk ParseType(type_info_string);
};
tmp_bind_markers[nb_tmp_bind_markers].typed = typ;
nb_tmp_bind_markers += 1;
},
else => {
current_bind_marker_type[current_bind_marker_type_pos] = c;
current_bind_marker_type_pos += 1;
},
},
else => {
@compileError("invalid state " ++ @tagName(state));
},
}
}
// The last character was a bind marker prefix so this must be an untyped bind marker.
switch (state) {
.bind_marker => {
tmp_bind_markers[nb_tmp_bind_markers].typed = null;
nb_tmp_bind_markers += 1;
},
.bind_marker_identifier => {
const new_buf = buf;
tmp_bind_markers[nb_tmp_bind_markers].name = @as([]const u8, new_buf[hold_pos - 1 .. pos]);
nb_tmp_bind_markers += 1;
},
.start => {},
else => @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote an incomplete bind marker type"),
}
const final_bind_markers = tmp_bind_markers;
const final_bind_markers_len = nb_tmp_bind_markers;
const final_buf = buf;
const final_query_len = pos;
return .{
.bind_markers = final_bind_markers,
.bind_markers_len = final_bind_markers_len,
.query = final_buf,
.query_len = final_query_len,
};
}
};
}
fn ParseType(comptime type_info: []const u8) type {
if (type_info.len <= 0) @compileError("invalid type info " ++ type_info);
// Integer
if (mem.eql(u8, "usize", type_info)) return usize;
if (mem.eql(u8, "isize", type_info)) return isize;
if (type_info[0] == 'u' or type_info[0] == 'i') {
const signedness = switch (type_info[0]) {
'u' => .unsigned,
'i' => .signed,
else => unreachable,
};
return @Int(signedness, std.fmt.parseInt(usize, type_info[1..type_info.len], 10) catch {
@compileError("invalid type info " ++ type_info);
});
}
// Float
if (mem.eql(u8, "f16", type_info)) return f16;
if (mem.eql(u8, "f32", type_info)) return f32;
if (mem.eql(u8, "f64", type_info)) return f64;
if (mem.eql(u8, "f128", type_info)) return f128;
// Bool
if (mem.eql(u8, "bool", type_info)) return bool;
// Strings
if (mem.eql(u8, "[]const u8", type_info) or mem.eql(u8, "[]u8", type_info)) {
return []const u8;
}
if (mem.eql(u8, "text", type_info)) return Text;
if (mem.eql(u8, "blob", type_info)) return Blob;
@compileError("invalid type info " ++ type_info);
}
test "parsed query: query" {
const testCase = struct {
query: []const u8,
expected_query: []const u8,
};
const testCases = &[_]testCase{
.{
.query = "INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{u32})",
.expected_query = "INSERT INTO user(id, name, age) VALUES(?, ?, ?)",
},
.{
.query = "SELECT id, name, age FROM user WHER age > ?{u32} AND age < ?{u32}",
.expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?",
},
.{
.query = "SELECT id, name, age FROM user WHER age > ? AND age < ?",
.expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?",
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
const parsed_query = ParsedQuery(tc.query);
try testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery());
}
}
test "parsed query: bind markers types" {
const testCase = struct {
query: []const u8,
expected_marker: BindMarker,
};
const prefixes = &[_][]const u8{
"?",
"?123",
":",
":hello",
"$",
"$foobar",
"@",
"@name",
};
inline for (prefixes) |prefix| {
const testCases = &[_]testCase{
.{
.query = "foobar " ++ prefix ++ "{usize}",
.expected_marker = .{ .typed = usize },
},
.{
.query = "foobar " ++ prefix ++ "{text}",
.expected_marker = .{ .typed = Text },
},
.{
.query = "foobar " ++ prefix ++ "{blob}",
.expected_marker = .{ .typed = Blob },
},
.{
.query = "foobar " ++ prefix,
.expected_marker = .{ .typed = null },
},
.{
.query = "foobar " ++ prefix ++ "{?[]const u8}",
.expected_marker = .{ .typed = ?[]const u8 },
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
const parsed_query = comptime ParsedQuery(tc.query);
try testing.expectEqual(1, parsed_query.bind_markers.len);
const bind_marker = parsed_query.bind_markers[0];
try testing.expectEqual(tc.expected_marker.typed, bind_marker.typed);
}
}
}
test "parsed query: bind markers identifier" {
const testCase = struct {
query: []const u8,
expected_marker: BindMarker,
};
const testCases = &[_]testCase{
.{
.query = "foobar @ABC{usize}",
.expected_marker = .{ .typed = usize },
},
.{
.query = "foobar ?123{text}",
.expected_marker = .{ .typed = Text },
},
.{
.query = "foobar $abc{blob}",
.expected_marker = .{ .typed = Blob },
},
.{
.query = "foobar :430{u32}",
.expected_marker = .{ .typed = u32 },
},
.{
.query = "foobar ?123",
.expected_marker = .{ .typed = null, .name = "123" },
},
.{
.query = "foobar :hola",
.expected_marker = .{ .typed = null, .name = "hola" },
},
.{
.query = "foobar @foo",
.expected_marker = .{ .typed = null, .name = "foo" },
},
};
inline for (testCases) |tc| {
const parsed_query = comptime ParsedQuery(tc.query);
try testing.expectEqual(@as(usize, 1), parsed_query.bind_markers.len);
const bind_marker = parsed_query.bind_markers[0];
if (bind_marker.name) |name| {
try testing.expectEqualStrings(tc.expected_marker.name.?, name);
}
try testing.expectEqual(tc.expected_marker.typed, bind_marker.typed);
}
}
test "parsed query: query bind identifier" {
const testCase = struct {
query: []const u8,
expected_query: []const u8,
expected_nb_bind_markers: usize,
};
const testCases = &[_]testCase{
.{
.query = "INSERT INTO user(id, name, age) VALUES(@id{usize}, :name{[]const u8}, $age{u32})",
.expected_query = "INSERT INTO user(id, name, age) VALUES(@id, :name, $age)",
.expected_nb_bind_markers = 3,
},
.{
.query = "INSERT INTO user(id, name, age) VALUES($id, $name, $age)",
.expected_query = "INSERT INTO user(id, name, age) VALUES($id, $name, $age)",
.expected_nb_bind_markers = 3,
},
.{
.query = "SELECT id, name, age FROM user WHER age > :ageGT{u32} AND age < @ageLT{u32}",
.expected_query = "SELECT id, name, age FROM user WHER age > :ageGT AND age < @ageLT",
.expected_nb_bind_markers = 2,
},
.{
.query = "SELECT id, name, age FROM user WHER age > :ageGT AND age < $ageLT",
.expected_query = "SELECT id, name, age FROM user WHER age > :ageGT AND age < $ageLT",
.expected_nb_bind_markers = 2,
},
.{
.query = "SELECT id, name, age FROM user WHER age > $my_age{i32} AND age < :your_age{i32}",
.expected_query = "SELECT id, name, age FROM user WHER age > $my_age AND age < :your_age",
.expected_nb_bind_markers = 2,
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
comptime {
const parsed_query = ParsedQuery(tc.query);
try testing.expectEqual(tc.expected_nb_bind_markers, parsed_query.bind_markers.len);
try testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery());
}
}
}
test "parsed query: bind marker character inside string" {
const testCase = struct {
query: []const u8,
exp_bind_markers: comptime_int,
exp: []const u8,
};
const testCases = &[_]testCase{
.{
.query = "SELECT json_extract(metadata, '$.name') AS name FROM foobar",
.exp_bind_markers = 0,
.exp = "SELECT json_extract(metadata, '$.name') AS name FROM foobar",
},
.{
.query = "SELECT json_extract(metadata, '$.name') AS name FROM foobar WHERE name = $name{text}",
.exp_bind_markers = 1,
.exp = "SELECT json_extract(metadata, '$.name') AS name FROM foobar WHERE name = $name",
},
.{
.query = "SELECT json_extract(metadata, '$[0]') AS name FROM foobar",
.exp_bind_markers = 0,
.exp = "SELECT json_extract(metadata, '$[0]') AS name FROM foobar",
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
const parsed_query = ParsedQuery(tc.query);
try testing.expectEqual(@as(usize, tc.exp_bind_markers), parsed_query.bind_markers.len);
try testing.expectEqualStrings(tc.exp, parsed_query.getQuery());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
const std = @import("std");
const build_options = @import("build_options");
const mem = std.mem;
const testing = std.testing;
const Db = @import("sqlite.zig").Db;
pub fn getTestDb() !Db {
var buf: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const mode = dbMode(fba.allocator());
return try Db.init(.{
.open_flags = .{
.write = true,
.create = true,
},
.mode = mode,
});
}
fn tmpDbPath(allocator: mem.Allocator) ![:0]const u8 {
const tmp_dir = testing.tmpDir(.{});
const path = try std.fs.path.join(allocator, &[_][]const u8{
"zig-cache",
"tmp",
&tmp_dir.sub_path,
"zig-sqlite.db",
});
defer allocator.free(path);
return allocator.dupeZ(u8, path);
}
fn dbMode(allocator: mem.Allocator) Db.Mode {
return if (build_options.in_memory) blk: {
break :blk .{ .Memory = {} };
} else blk: {
if (build_options.dbfile) |dbfile| {
return .{ .File = allocator.dupeZ(u8, dbfile) catch unreachable };
}
const path = tmpDbPath(allocator) catch unreachable;
std.fs.cwd().deleteFile(path) catch {};
break :blk .{ .File = path };
};
}

File diff suppressed because it is too large Load Diff

16
zig-vendor/zig-sqlite/zig.mod vendored Normal file
View File

@@ -0,0 +1,16 @@
id: nj8usqhaks6kkewaj3pbp0arfh4281me25bl7tf9das1vbqv
name: sqlite
main: sqlite.zig
license: MIT
description: Thin SQLite wrapper
c_include_dirs:
- c
c_source_files:
- c/workaround.c
dependencies:
- src: http https://sqlite.org/2025/sqlite-amalgamation-3480000.zip sha256-d9a15a42db7c78f88fe3d3c5945acce2f4bfe9e4da9f685cd19f6ea1d40aa884
license: blessing
c_include_dirs:
- sqlite-amalgamation-3480000
c_source_files:
- sqlite-amalgamation-3480000/sqlite3.c

View File

@@ -0,0 +1 @@
2