Add Highlight struct with SHA-256 hashing and improve query
- Add Highlight struct to represent bookmarks with owned memory - Implement SHA-256 hashing for text and book identification - Update SQL query to extract clean book filenames (removes path and fragment) - Remove unused Annotation column from query - Add CLAUDE.md for repository documentation - Update .gitignore to exclude SQLite WAL files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,8 @@ kb_exfiltrator-kobo-arm
|
|||||||
local-dev.sqlite
|
local-dev.sqlite
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.db
|
*.db
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
|
||||||
# Editor/IDE files
|
# Editor/IDE files
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
A statically-linked Zig binary that extracts highlights from Kobo e-reader SQLite databases. The project builds for both x86_64 (development) and ARM (target Kobo device).
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Local development (x86_64)
|
||||||
|
```bash
|
||||||
|
zig build
|
||||||
|
./zig-out/bin/kb_exfiltrator local-dev.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run with arguments
|
||||||
|
```bash
|
||||||
|
zig build run -- path/to/database.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
zig build test
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Kobo (ARM target)
|
||||||
|
```bash
|
||||||
|
./build-kobo.sh
|
||||||
|
# or manually:
|
||||||
|
zig build -Dtarget=arm-linux-musleabihf -Doptimize=ReleaseSmall
|
||||||
|
```
|
||||||
|
|
||||||
|
The ARM build creates `kb_exfiltrator-kobo-arm` in the project root.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Module Structure
|
||||||
|
|
||||||
|
The project uses Zig's module system with two distinct modules:
|
||||||
|
|
||||||
|
- **kb_exfiltrator module** (`src/root.zig`): Library module exposing reusable functionality
|
||||||
|
- **Executable module** (`src/main.zig`): CLI entry point that imports the library module
|
||||||
|
|
||||||
|
The executable depends on the library module via the imports mechanism in build.zig:87-91.
|
||||||
|
|
||||||
|
### SQLite Integration
|
||||||
|
|
||||||
|
SQLite is vendored as an amalgamation build (vendor/sqlite-amalgamation-3480000/). The build.zig configuration:
|
||||||
|
- Links SQLite as a C source file (build.zig:87-93)
|
||||||
|
- Compiles with `SQLITE_THREADSAFE=0` (no threading, smaller binary)
|
||||||
|
- Compiles with `SQLITE_OMIT_LOAD_EXTENSION` (security)
|
||||||
|
- Uses `@cImport` in main.zig:2-4 to import C headers
|
||||||
|
|
||||||
|
### Static Linking
|
||||||
|
|
||||||
|
The binary is fully statically linked:
|
||||||
|
- Uses musl libc for ARM target (`arm-linux-musleabihf`)
|
||||||
|
- SQLite compiled directly into the binary
|
||||||
|
- No external dependencies required on target device
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
The program queries the Kobo `Bookmark` table:
|
||||||
|
- `Text`: The highlighted text (non-NULL indicates a highlight)
|
||||||
|
- `Annotation`: User's notes on the highlight (nullable)
|
||||||
|
- `DateCreated`: Timestamp
|
||||||
|
- `ContentID`: Book identifier
|
||||||
|
|
||||||
|
Query implementation: src/main.zig:33-42
|
||||||
|
|
||||||
|
### Build System Details
|
||||||
|
|
||||||
|
The build.zig uses Zig's build graph system:
|
||||||
|
- Target and optimization are configurable via CLI (`-Dtarget`, `-Doptimize`)
|
||||||
|
- The executable step (build.zig:60-84) creates the root module and wires dependencies
|
||||||
|
- Tests are split into module tests and executable tests (build.zig:132-154)
|
||||||
|
- Both test suites run in parallel when executing `zig build test`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Zig 0.15.2 or later (specified in build.zig.zon:28)
|
||||||
+50
-21
@@ -3,6 +3,46 @@ const c = @cImport({
|
|||||||
@cInclude("sqlite3.h");
|
@cInclude("sqlite3.h");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Highlight = struct {
|
||||||
|
text: []const u8, // Owned copy
|
||||||
|
date: []const u8, // Owned copy
|
||||||
|
book: []const u8, // Owned copy
|
||||||
|
text_hash: [64]u8, // Stack allocated - no cleanup needed
|
||||||
|
book_hash: [64]u8, // Stack allocated - no cleanup needed
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
fn init(allocator: std.mem.Allocator, text: []const u8, date: []const u8, book: []const u8, content_id_raw: []const u8) !Highlight {
|
||||||
|
var highlight = Highlight{
|
||||||
|
.text = try allocator.dupe(u8, text), // Copy from SQLite buffer
|
||||||
|
.date = try allocator.dupe(u8, date), // Copy from SQLite buffer
|
||||||
|
.book = try allocator.dupe(u8, book), // Copy from SQLite buffer
|
||||||
|
.text_hash = undefined,
|
||||||
|
.book_hash = undefined,
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now compute hashes into the struct fields
|
||||||
|
computeHash(text, &highlight.text_hash);
|
||||||
|
computeHash(content_id_raw, &highlight.book_hash);
|
||||||
|
|
||||||
|
return highlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *Highlight) void {
|
||||||
|
self.allocator.free(self.text);
|
||||||
|
self.allocator.free(self.date);
|
||||||
|
self.allocator.free(self.book);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn computeHash(data: []const u8, out_hex: *[64]u8) void {
|
||||||
|
var hash: [32]u8 = undefined; // SHA256 produces 32 bytes
|
||||||
|
std.crypto.hash.sha2.Sha256.hash(data, &hash, .{});
|
||||||
|
|
||||||
|
// Convert 32 bytes to 64 hex characters
|
||||||
|
_ = std.fmt.bufPrint(out_hex, "{s}", .{std.fmt.fmtSliceHexLower(&hash)}) catch unreachable;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
@@ -33,9 +73,11 @@ pub fn main() !void {
|
|||||||
const query =
|
const query =
|
||||||
\\SELECT
|
\\SELECT
|
||||||
\\ Text,
|
\\ Text,
|
||||||
\\ Annotation,
|
|
||||||
\\ DateCreated,
|
\\ DateCreated,
|
||||||
\\ ContentID
|
\\ SUBSTR(
|
||||||
|
\\ SUBSTR(ContentID, 40, INSTR(ContentID, '.epub') - 40 + 5),
|
||||||
|
\\ INSTR(SUBSTR(ContentID, 40, INSTR(ContentID, '.epub') - 40 + 5), '/') + 1
|
||||||
|
\\ ) AS Book
|
||||||
\\FROM Bookmark
|
\\FROM Bookmark
|
||||||
\\WHERE Text IS NOT NULL
|
\\WHERE Text IS NOT NULL
|
||||||
\\ORDER BY DateCreated DESC;
|
\\ORDER BY DateCreated DESC;
|
||||||
@@ -66,38 +108,25 @@ pub fn main() !void {
|
|||||||
else
|
else
|
||||||
"";
|
"";
|
||||||
|
|
||||||
// Get annotation (user's note, may be NULL)
|
|
||||||
const annotation_ptr = c.sqlite3_column_text(stmt, 1);
|
|
||||||
const annotation = if (annotation_ptr != null)
|
|
||||||
std.mem.span(@as([*:0]const u8, @ptrCast(annotation_ptr)))
|
|
||||||
else
|
|
||||||
null;
|
|
||||||
|
|
||||||
// Get date
|
// Get date
|
||||||
const date_ptr = c.sqlite3_column_text(stmt, 2);
|
const date_ptr = c.sqlite3_column_text(stmt, 1);
|
||||||
const date = if (date_ptr != null)
|
const date = if (date_ptr != null)
|
||||||
std.mem.span(@as([*:0]const u8, @ptrCast(date_ptr)))
|
std.mem.span(@as([*:0]const u8, @ptrCast(date_ptr)))
|
||||||
else
|
else
|
||||||
"unknown";
|
"unknown";
|
||||||
|
|
||||||
// Get content ID (book identifier)
|
// Get book title
|
||||||
const content_ptr = c.sqlite3_column_text(stmt, 3);
|
const book_ptr = c.sqlite3_column_text(stmt, 2);
|
||||||
const content_id = if (content_ptr != null)
|
const book = if (book_ptr != null)
|
||||||
std.mem.span(@as([*:0]const u8, @ptrCast(content_ptr)))
|
std.mem.span(@as([*:0]const u8, @ptrCast(book_ptr)))
|
||||||
else
|
else
|
||||||
"unknown";
|
"unknown";
|
||||||
|
|
||||||
// Print formatted output
|
// Print formatted output
|
||||||
try stdout.print("\n========== Highlight #{d} ==========\n", .{count});
|
try stdout.print("\n========== Highlight #{d} ==========\n", .{count});
|
||||||
try stdout.print("Date: {s}\n", .{date});
|
try stdout.print("Date: {s}\n", .{date});
|
||||||
try stdout.print("Book: {s}\n", .{content_id});
|
try stdout.print("Book: {s}\n", .{book});
|
||||||
try stdout.print("\n{s}\n", .{text});
|
try stdout.print("\n{s}\n", .{text});
|
||||||
|
|
||||||
if (annotation) |note| {
|
|
||||||
if (note.len > 0) {
|
|
||||||
try stdout.print("\nNote: {s}\n", .{note});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try stdout.print("===================================\n", .{});
|
try stdout.print("===================================\n", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user