diff --git a/fixtures/encrypted-single-file.db.age b/fixtures/encrypted-single-file.db.age new file mode 100644 index 0000000..613d717 Binary files /dev/null and b/fixtures/encrypted-single-file.db.age differ diff --git a/src/Db.zig b/src/Db.zig index bda2903..7764312 100644 --- a/src/Db.zig +++ b/src/Db.zig @@ -40,18 +40,20 @@ pub fn open( defer gpa.free(tmp_db_path); // TODO: Use std.MultiArrayList? Had json issues - var private_keys: std.ArrayList([]const u8) = try .initCapacity( - gpa, - opts.config.keys.len, - ); + { + var private_keys: std.ArrayList([]const u8) = try .initCapacity( + gpa, + opts.config.keys.len, + ); + defer private_keys.deinit(gpa); - for (opts.config.keys) |key| { - private_keys.appendAssumeCapacity(key.private); + for (opts.config.keys) |key| { + private_keys.appendAssumeCapacity(key.private); + } + + // TODO: Pass key(s) from Config + try age.decrypt(io, gpa, private_keys.items, db_path, tmp_db_path); } - - // TODO: Pass key(s) from Config - try age.decrypt(io, gpa, private_keys.items, db_path, tmp_db_path); - try db.restore(tmp_db_path); try std.Io.Dir.cwd().deleteFile(io, tmp_db_path); @@ -75,7 +77,10 @@ const OpenOptions = struct { fn new(config: Config) !@This() { var db = try sqlite.Db.init(.{ .mode = .Memory, - .open_flags = .{ .write = true, .create = true }, + .open_flags = .{ + .write = true, + .create = true, + }, .threading_mode = .MultiThread, }); @@ -114,13 +119,21 @@ fn restore( .{}, .{path}, ); + std.debug.print("path: {s}\n", .{path}); defer self.sql_db.exec("DETACH DATABASE source", .{}, .{}) catch unreachable; - try self.sql_db.exec( + var diags: sqlite.Diagnostics = .{}; + self.sql_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; + }; } // TODO: Finish @@ -158,6 +171,7 @@ pub fn close( gpa, opts.config.keys.len, ); + defer public_keys.deinit(gpa); for (opts.config.keys) |key| { public_keys.appendAssumeCapacity(key.private); @@ -171,21 +185,24 @@ pub fn close( /// Returns a list of all the .env files present in the database. /// The caller is responsible for freeing memory -fn list(self: @This(), gpa: std.mem.Allocator) ![]EnvFile { - var stmt = self.sql_db.prepare( +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([]const EnvFile, gpa, .{}, .{}); + return stmt.all(EnvFile, gpa, .{}, .{}); } 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, - remotes: [][]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, }; @@ -269,7 +286,6 @@ test "Closing a fresh database does not create a file" { var tmp_dir = std.testing.tmpDir(.{}); defer tmp_dir.cleanup(); - // @compileLog(@typeInfo(std.Io.File.Permissions)); try tmp_dir.dir.createDir(io, "home", .default_dir); try tmp_dir.dir.createDir(io, "tmp", .default_dir); @@ -301,6 +317,167 @@ test "Closing a fresh database does not create a file" { ); } -// test "Closing an unmodified database does not update the file" {} +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); + try std.testing.expectEqual(1, env_files.len); + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + for (env_files) |file| { + try std.testing.expectEqualSlices(u8, "", file.path); + try std.testing.expectEqualSlices(u8, "", file.contents); + try std.testing.expectEqualSlices(u8, "", file.remotes); + + hasher.update(file.contents); + const hash = hasher.finalResult(); + try std.testing.expectEqualSlices(u8, &hash, file.sha256); + } else { + return error.TestUnexpectedResult; + } + + try db.close(io, gpa, .{ .home = home, .tmp = tmp }); +} diff --git a/zig-vendor/zig-sqlite/query.zig b/zig-vendor/zig-sqlite/query.zig index 2717f7f..6037375 100644 --- a/zig-vendor/zig-sqlite/query.zig +++ b/zig-vendor/zig-sqlite/query.zig @@ -226,6 +226,7 @@ fn ParseType(comptime type_info: []const u8) type { 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); diff --git a/zig-vendor/zig-sqlite/sqlite.zig b/zig-vendor/zig-sqlite/sqlite.zig index 1702a0b..5fc22dc 100644 --- a/zig-vendor/zig-sqlite/sqlite.zig +++ b/zig-vendor/zig-sqlite/sqlite.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const build_options = @import("build_options"); const debug = std.debug; const heap = std.heap; -const io = std.io; +const io = std.Io; const mem = std.mem; const testing = std.testing; @@ -1967,7 +1967,7 @@ pub const DynamicStatement = struct { pub fn all(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, values: anytype) ![]Type { var iter = try self.iteratorAlloc(Type, allocator, values); - var rows: std.ArrayList(Type) = .{}; + var rows: std.ArrayList(Type) = .empty; while (try iter.nextAlloc(allocator, options)) |row| { try rows.append(allocator, row); } @@ -2257,7 +2257,7 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: anytype) type pub fn all(self: *Self, comptime Type: type, allocator: mem.Allocator, options: QueryOptions, values: anytype) ![]Type { var iter = try self.iteratorAlloc(Type, allocator, values); - var rows: std.ArrayList(Type) = .{}; + var rows: std.ArrayList(Type) = .empty; while (try iter.nextAlloc(allocator, options)) |row| { try rows.append(allocator, row); } @@ -3020,7 +3020,7 @@ test "sqlite: statement iterator" { var stmt = try db.prepare("INSERT INTO user(name, id, age, weight, favorite_color) VALUES(?{[]const u8}, ?{usize}, ?{usize}, ?{f32}, ?{[]const u8})"); defer stmt.deinit(); - var expected_rows: std.ArrayList(TestUser) = .{}; + var expected_rows: std.ArrayList(TestUser) = .empty; var i: usize = 0; while (i < 20) : (i += 1) { const name = try std.fmt.allocPrint(allocator, "Vincent {d}", .{i}); @@ -3047,7 +3047,7 @@ test "sqlite: statement iterator" { var iter = try stmt2.iterator(RowType, .{}); - var rows: std.ArrayList(RowType) = .{}; + var rows: std.ArrayList(RowType) = .empty; while (try iter.next(.{})) |row| { try rows.append(allocator, row); } @@ -3074,7 +3074,7 @@ test "sqlite: statement iterator" { var iter = try stmt2.iterator(RowType, .{}); - var rows: std.ArrayList(RowType) = .{}; + var rows: std.ArrayList(RowType) = .empty; while (try iter.nextAlloc(allocator, .{})) |row| { try rows.append(allocator, row); } @@ -3459,7 +3459,7 @@ test "sqlite: bind runtime slice" { const allocator = arena.allocator(); // creating array list on heap so that it's deemed runtime size - var list: std.ArrayList([]const u8) = .{}; + var list: std.ArrayList([]const u8) = .empty; defer list.deinit(allocator); try list.append(allocator, "this is some data"); const args = try list.toOwnedSlice(allocator); @@ -3810,7 +3810,11 @@ test "sqlite: create aggregate function with an aggregate context" { var db = try getTestDb(); defer db.deinit(); - var rand = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp())); + const test_io = std.testing.io; + + var rand = std.Random.DefaultPrng.init( + @intCast(std.Io.Timestamp.now(test_io, .real).toMilliseconds()), + ); try db.createAggregateFunction( "mySum", @@ -3877,7 +3881,7 @@ test "sqlite: empty slice" { defer db.deinit(); try addTestData(&db); - var list: std.ArrayList(u8) = .{}; + var list: std.ArrayList(u8) = .empty; const ptr = try list.toOwnedSlice(allocator); try db.exec("INSERT INTO article(author_id, data) VALUES(?, ?)", .{}, .{ 1, ptr }); @@ -4054,7 +4058,7 @@ test "reuse same field twice in query string" { test "fuzzing" { const Context = struct { - fn testOne(_: @This(), input: []const u8) anyerror!void { + fn testOne(_: @This(), input: *testing.Smith) anyerror!void { var db = try Db.init(.{ .mode = .Memory, .open_flags = .{ @@ -4066,7 +4070,7 @@ test "fuzzing" { try db.exec("CREATE TABLE test(id integer primary key, name text, data blob)", .{}, .{}); - db.execDynamic(input, .{}, .{}) catch |err| switch (err) { + db.execDynamic(input.value([]const u8), .{}, .{}) catch |err| switch (err) { error.SQLiteError => return, error.ExecReturnedData => return, error.EmptyQuery => return, diff --git a/zig-vendor/zig-sqlite/vtab.zig b/zig-vendor/zig-sqlite/vtab.zig index 19826d0..65e2d6a 100644 --- a/zig-vendor/zig-sqlite/vtab.zig +++ b/zig-vendor/zig-sqlite/vtab.zig @@ -766,7 +766,8 @@ pub fn VirtualTable( // - const nullable_state: ?*State = @fieldParentPtr("vtab", vtab); + const vtab_ptr: *c.sqlite3_vtab = @ptrCast(vtab); + const nullable_state: ?*State = @fieldParentPtr("vtab", vtab_ptr); const state = nullable_state orelse unreachable; var arena = heap.ArenaAllocator.init(state.module_context.allocator); @@ -789,7 +790,8 @@ pub fn VirtualTable( } fn xDisconnect(vtab: [*c]c.sqlite3_vtab) callconv(.c) c_int { - const nullable_state: ?*State = @fieldParentPtr("vtab", vtab); + const vtab_ptr: *c.sqlite3_vtab = @ptrCast(vtab); + const nullable_state: ?*State = @fieldParentPtr("vtab", vtab_ptr); const state = nullable_state orelse unreachable; state.deinit(); @@ -806,7 +808,8 @@ pub fn VirtualTable( } fn xOpen(vtab: [*c]c.sqlite3_vtab, vtab_cursor: [*c][*c]c.sqlite3_vtab_cursor) callconv(.c) c_int { - const nullable_state: ?*State = @fieldParentPtr("vtab", vtab); + const vtab_ptr: *c.sqlite3_vtab = @ptrCast(vtab); + const nullable_state: ?*State = @fieldParentPtr("vtab", vtab_ptr); const state = nullable_state orelse unreachable; const cursor_state = CursorState.init(state.module_context, state.table) catch |err| { @@ -819,7 +822,8 @@ pub fn VirtualTable( } fn xClose(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.c) c_int { - const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); + const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor); + const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr); const cursor_state = nullable_cursor_state orelse unreachable; cursor_state.deinit(); @@ -828,7 +832,8 @@ pub fn VirtualTable( } fn xEof(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.c) c_int { - const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); + const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor); + const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr); const cursor_state = nullable_cursor_state orelse unreachable; const cursor = cursor_state.cursor; @@ -866,7 +871,8 @@ pub fn VirtualTable( } fn xFilter(vtab_cursor: [*c]c.sqlite3_vtab_cursor, idx_num: c_int, idx_str: [*c]const u8, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) c_int { - const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); + const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor); + const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr); const cursor_state = nullable_cursor_state orelse unreachable; const cursor = cursor_state.cursor; @@ -892,7 +898,8 @@ pub fn VirtualTable( } fn xNext(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.c) c_int { - const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); + const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor); + const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr); const cursor_state = nullable_cursor_state orelse unreachable; const cursor = cursor_state.cursor; @@ -911,7 +918,8 @@ pub fn VirtualTable( } fn xColumn(vtab_cursor: [*c]c.sqlite3_vtab_cursor, ctx: ?*c.sqlite3_context, n: c_int) callconv(.c) c_int { - const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); + const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor); + const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr); const cursor_state = nullable_cursor_state orelse unreachable; const cursor = cursor_state.cursor; @@ -955,7 +963,8 @@ pub fn VirtualTable( } fn xRowid(vtab_cursor: [*c]c.sqlite3_vtab_cursor, row_id_ptr: [*c]c.sqlite3_int64) callconv(.c) c_int { - const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); + const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor); + const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr); const cursor_state = nullable_cursor_state orelse unreachable; const cursor = cursor_state.cursor; @@ -1023,7 +1032,9 @@ const TestVirtualTable = struct { // const data = &[_][]const u8{ - "Vincent", "José", "Michel", + "Vincent", + "José", + "Michel", }; var rand = std.Random.DefaultPrng.init(204882485); @@ -1064,13 +1075,18 @@ const TestVirtualTable = struct { debug.print("connect\n", .{}); } - pub const BuildBestIndexError = error{} || mem.Allocator.Error; + pub const BuildBestIndexError = error{} || mem.Allocator.Error || error{WriteFailed}; - pub fn buildBestIndex(self: *TestVirtualTable, diags: *VTabDiagnostics, builder: *BestIndexBuilder) BuildBestIndexError!void { + pub fn buildBestIndex( + self: *TestVirtualTable, + diags: *VTabDiagnostics, + builder: *BestIndexBuilder, + ) BuildBestIndexError!void { _ = self; _ = diags; - var id_str_writer = builder.id_str_buffer.writer(builder.allocator); + // var id_str_writer = builder.id_str_buffer.writer(builder.allocator); + var id_str_writer = std.Io.Writer.fromArrayList(&builder.id_str_buffer); var argv_index: i32 = 0; for (builder.constraints) |*constraint| { @@ -1174,7 +1190,7 @@ const TestVirtualTableCursor = struct { // 3 chars for the '=' marker // 6 chars because we format all columns in a 6 char wide string const col_str = id[pos + 1 .. pos + 1 + 6]; - const col = try fmt.parseInt(i32, mem.trimRight(u8, col_str, " "), 10); + const col = try fmt.parseInt(i32, mem.trimEnd(u8, col_str, " "), 10); id = id[pos + 1 + 6 ..];