From d104d5d936a7854e3ba60d6963213f0b41e76e54 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 16 Oct 2026 00:45:38 +0000 Subject: [PATCH] Escape state_dir in the db_url derived from it (closes #290) The derived db_url put state_dir into a file: URL as it was, so SQLite decoded a %HH in the name and ended the path at a ? or #, and the journal mode parameter was lost. It is now built with net/url, which percent-encodes the directory name; SQLite decodes it back and opens state.sqlite3 in that directory. OmitHost keeps the usual form file:/var/lib/pixa/state.sqlite3 and a relative state_dir a path. Model: opus-5-5 --- README.md | 12 +++++++----- TODO.md | 6 ++++++ internal/config/config.go | 21 +++++++++++++++++++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d9b3c86..17ceda1 100644 --- a/README.md +++ b/README.md @@ -725,11 +725,13 @@ Key settings in more detail: - `signing_key` — HMAC secret for URL signatures - `db_url` — the SQLite database to open; omitted, it is `file:/state.sqlite3?_pragma=journal_mode(WAL)`, which keeps the - database in WAL mode. pixa opens one connection to it, so its own reads and - writes run one at a time. pixa adds `_pragma=busy_timeout(5000)` to any - `db_url`, so a write that finds another program writing to the file waits up - to five seconds for it instead of failing. WAL mode comes only from the URL: - keep `_pragma=journal_mode(WAL)` in one you set + database in WAL mode. `state_dir` is percent-encoded in it, so a `%`, `?` or + `#` in the name is written as `%25`, `%3F` or `%23`. pixa opens one connection + to it, so its own reads and writes run one at a time. pixa adds + `_pragma=busy_timeout(5000)` to any `db_url`, so a write that finds another + program writing to the file waits up to five seconds for it instead of + failing. WAL mode comes only from the URL: keep `_pragma=journal_mode(WAL)` in + one you set - `cache_max_bytes` — disk cache size limit in bytes; `0` disables the disk cache entirely; omitted defaults to 75% of the sum of the free space on the filesystem containing `/cache/` and the bytes of source and diff --git a/TODO.md b/TODO.md index a00b1ef..e8044db 100644 --- a/TODO.md +++ b/TODO.md @@ -62,6 +62,12 @@ P2: security: per-origin rate limiting another way, such as with `%2F` in the path, whatever else the path holds, or with a user name and password. It also answers 400 for a source path holding a `..` segment, as `/v1/image/` refuses one. +- 2026-10-16 a `state_dir` whose name holds `%`, `?` or `#` keeps its database + at `state.sqlite3` in that directory (closes #290): the `db_url` derived from + it put the name into a `file:` URL as it was, so SQLite read `%41` as `A`, a + `?` or `#` ended the path early, and the journal mode parameter was lost. The + derived `db_url` is now built with Go's `net/url`, which writes those + characters as `%25`, `%3F` and `%23`. - 2026-10-15 a thin source requested with `fit=cover` no longer makes libvips build a huge image (closes #282): libvips' thumbnail scaled the whole source to cover the requested size and copied that into memory before it cut out the diff --git a/internal/config/config.go b/internal/config/config.go index ff4ac6c..d94fb74 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -338,8 +338,7 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) { settingName(keyDBURL), errValueEmpty) } - // The driver sets the journal mode only through a _pragma parameter. - c.DBURL = fmt.Sprintf("file:%s/state.sqlite3?_pragma=journal_mode(WAL)", c.StateDir) + c.DBURL = defaultDBURL(c.StateDir) } if loader.err != nil { @@ -354,6 +353,24 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) { return c, nil } +// defaultDBURL returns the db_url used when db_url is omitted: the file +// state.sqlite3 in stateDir, in WAL mode, which the driver sets only +// through a _pragma parameter. String() percent-encodes any %, ? or # in +// the directory name, which SQLite decodes back, so the URL names exactly +// this file. OmitHost writes file:/var/lib/pixa/... instead of +// file:///var/lib/pixa/..., and keeps a relative stateDir from being read +// as a host name. +func defaultDBURL(stateDir string) string { + dbURL := url.URL{ + Scheme: "file", + OmitHost: true, + Path: stateDir + "/state.sqlite3", + RawQuery: "_pragma=journal_mode(WAL)", + } + + return dbURL.String() +} + // validateKnownKeys rejects configuration files containing keys the // application does not understand, so typos fail at startup instead of // being silently ignored, and rejects keys that are explicitly set to