diff --git a/fixtures/encrypted-single-file.db.age b/fixtures/encrypted-single-file.db.age new file mode 100644 index 0000000..02d1c4c Binary files /dev/null and b/fixtures/encrypted-single-file.db.age differ diff --git a/fixtures/single-file.db b/fixtures/single-file.db index 2095cd0..143c398 100644 Binary files a/fixtures/single-file.db and b/fixtures/single-file.db differ diff --git a/src/Db.zig b/src/Db.zig index bda2903..9344eef 100644 --- a/src/Db.zig +++ b/src/Db.zig @@ -30,35 +30,31 @@ pub fn open( }); defer gpa.free(db_path); - var db = try new(opts.config); + // 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)) { - // 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.join(gpa, &.{ opts.tmp, "envr.db" }); - 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); - - return db; - } else { - return db; } + + return open_decrypted(opts.config, tmp_db_path); } const OpenOptions = struct { @@ -71,16 +67,19 @@ const OpenOptions = struct { tmp: []const u8 = "/tmp", }; -/// Create a new instance of the database in-memory -fn new(config: Config) !@This() { +/// Create a new instance of the database +fn open_decrypted(config: Config, tmp_db_path: [:0]const u8) !@This() { var db = try sqlite.Db.init(.{ - .mode = .Memory, - .open_flags = .{ .write = true, .create = true }, + .mode = .{ .File = tmp_db_path }, + .open_flags = .{ + .write = true, + .create = true, + }, .threading_mode = .MultiThread, }); try db.exec( - \\create table envr_env_files ( + \\create table if not exists envr_env_files ( \\ path text primary key not null \\, remotes text -- JSON \\, sha256 text not null @@ -103,26 +102,6 @@ fn db_exists(io: std.Io, path: []const u8) bool { } } -/// Loads the unencrypted sqlite db at filepath path into the datbase -/// FIXME: Test me -fn restore( - self: *@This(), - path: []const u8, -) !void { - try self.sql_db.exec( - "ATTACH DATABASE ? AS source", - .{}, - .{path}, - ); - defer self.sql_db.exec("DETACH DATABASE source", .{}, .{}) catch unreachable; - - try self.sql_db.exec( - "INSERT INTO main.envr_env_files SELECT * FROM source.envr_env_files", - .{}, - .{}, - ); -} - // TODO: Finish // pub fn tmpDir(opts: std.fs.Dir.OpenDirOptions) TmpDir { // var random_bytes: [TmpDir.random_bytes_count]u8 = undefined; @@ -153,41 +132,54 @@ pub fn close( const db_path = try std.fs.path.join(gpa, &.{ 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, - opts.config.keys.len, - ); + { + // TODO: Use std.MultiArrayList? Had json issues + var public_keys: std.ArrayList([]const u8) = try .initCapacity( + gpa, + opts.config.keys.len, + ); + defer public_keys.deinit(gpa); - for (opts.config.keys) |key| { - public_keys.appendAssumeCapacity(key.private); + for (opts.config.keys) |key| { + public_keys.appendAssumeCapacity(key.private); + } + + try age.encrypt(io, gpa, public_keys.items, tmp_db_path, db_path); } - 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 -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, + + 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 { @@ -269,7 +261,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 +292,182 @@ 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); + 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, .{ .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..2c1d343 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); @@ -3749,7 +3749,11 @@ test "sqlite: create aggregate function with no aggregate context" { var db = try getTestDb(); defer db.deinit(); - var rand = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp())); + const test_io = testing.io; + + var rand = std.Random.DefaultPrng.init(@intCast( + std.Io.Timestamp.now(test_io, .real).toMilliseconds(), + )); // Create an aggregate function working with a MyContext @@ -3810,7 +3814,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 +3885,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 +4062,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 +4074,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 ..];