Skip to content

Commit db6bfc2

Browse files
authored
feat: add asset_key_pattern to the s3 builders (#193)
Custom regex for deriving (name, version) from s3 object keys, so a pre-release key like mybin-0.1.2-beta-x86_64-... parses as 0.1.2-beta instead of the default matcher's lossy 0.1.2 (#61). The pattern must define name and version named capture groups; it is compiled and validated at build(), surfacing failures as the new Error::InvalidAssetKeyPattern. A custom capture that does not parse as semver skips the key, matching the forge backends' non-semver-tag skip. Unset keeps the default matcher unchanged.
1 parent 0630669 commit db6bfc2

5 files changed

Lines changed: 425 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
monorepo-style tag such as `myapp-1.2.3` (or `myapp-v1.2.3`). Defaults to unset, which trims a
2929
leading `v` as before; when set, tags without the prefix are skipped from the listing rather than
3030
mis-parsed. ([#76](https://github.com/jaemk/self_update/issues/76))
31+
- `asset_key_pattern(..)` on the s3 `Update`/`ReleaseList` builders: a custom regex for deriving
32+
`(name, version)` from object keys, replacing the built-in matcher whose version group only
33+
captures a `major.minor.patch` triple. Lets a pre-release key such as
34+
`mybin-0.1.2-beta-x86_64-unknown-linux-gnu` parse as `0.1.2-beta` instead of `0.1.2`. The
35+
pattern must define `name` and `version` named capture groups and is validated at `build()`
36+
(`Error::InvalidAssetKeyPattern`); a captured version that does not parse as semver skips the
37+
key. Unset keeps the existing matcher unchanged.
38+
([#61](https://github.com/jaemk/self_update/issues/61))
3139

3240
### Changed
3341
- A recognized-but-unsupported compression extension now fails loudly instead of silently

specs/ref-errors.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ code builds them via the public constructors (`http_status_error(404, ..)`,
5151
| `InvalidAssetName { name: String }` | The server-supplied asset name is empty, `.`, `..`, contains a `/` or `\` path separator, or is an absolute path; the file is never created (`update.rs`). `#[non_exhaustive]`. | none | no (struct fields) |
5252
| `SignatureNonUTF8` | Generated archive path contains non-UTF-8 characters so its signature cannot be verified. Unit variant. | `signatures` | no (unit) |
5353
| `S3Auth(Box<dyn Error + Send + Sync>)` | S3 SigV4 request-signing failure, including the host-extraction case (a signed URL with no extractable host). Via `From<SystemTimeError>`, `From<hmac::digest::InvalidLength>`, `From<url::ParseError>`, `From<time::error::ComponentRange>`, and direct construction at the host-extraction sites (`s3.rs`). | `s3-auth` | yes (boxed) |
54+
| `InvalidAssetKeyPattern { source: Box<dyn Error + Send + Sync> }` | A user-supplied `asset_key_pattern` on the s3 builders did not compile or lacks a required named capture group (`name` / `version`). Raised from `build()` via `compile_asset_key_pattern` (`s3.rs`); the source is the regex-compile error or a `MessageError` naming the missing group. `#[non_exhaustive]`. | `s3` | yes (boxed source) |
5455

5556
### Reclassification of construction sites
5657

@@ -141,6 +142,7 @@ Each variant renders with a specific Display string:
141142
- `Signature(e)` -> `"SignatureError: {e}"` (dereferences the box, `signatures`)
142143
- `SignatureNonUTF8` -> `"SignatureError: cannot verify signature of a file with a non-UTF-8 name"` (`signatures`)
143144
- `S3Auth(e)` -> `"S3AuthError: {e}"` (dereferences the box, `s3-auth`)
145+
- `InvalidAssetKeyPattern { source }` -> `"ConfigError: invalid asset_key_pattern: {source}"` (`s3`)
144146

145147
Note: `ArchiveNotEnabled` was corrected from `"ArchiveNotEnabled: ..."` to `"ArchiveNotEnabledError: ..."`;
146148
`SignatureNonUTF8` was corrected from the bare message to `"SignatureError: ..."`, consistent with
@@ -151,7 +153,8 @@ every other variant using a `<Name>Error:` prefix.
151153
`source()` returns the inner error for the wrapping variants: `Io` (the concrete io error); the
152154
boxed `Json`, `Transport`, `SemVer`, `Zip` (gated), `Signature` (gated), `S3Auth` (gated); the
153155
boxed-source variants `InvalidResponse`, `InvalidHeader`, `InvalidAuthToken`,
154-
`InvalidCertificate`, `InvalidProgressStyle` (gated); and `Internal` when its `source` is `Some`
156+
`InvalidCertificate`, `InvalidProgressStyle` (gated), `InvalidAssetKeyPattern` (gated); and
157+
`Internal` when its `source` is `Some`
155158
-- each via deref of the box. The `Internal { source: None }` form and all field-only variants
156159
(`VerificationRejected`, `ChecksumMismatch`, `Aborted`, `NotFound`, `Unauthorized`, `HttpStatus`,
157160
`NoReleaseFound`, `MissingAssetField`, `MissingField`, `ArchiveNotEnabled`,

specs/ref-s3-backend.md

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@ Two builders, each reached through a `configure()` entry point:
2323
`async` feature, `ReleaseList::fetch_async` (`s3.rs:282`). The result is a bare listing
2424
(`current_version()` is `None`); recover the `Vec<Release>` with `into_vec()`.
2525
`ReleaseList::configure` (`s3.rs:205`) seeds the builder. Setters: `bucket_name`,
26-
`asset_prefix`, `region`, `endpoint`, `filter_target`, `max_keys`, and (under `s3-auth`)
27-
`access_key` and `signature_ttl`; plus the shared `request_config_setters!(request)`.
26+
`asset_prefix`, `asset_key_pattern`, `region`, `endpoint`, `filter_target`, `max_keys`, and
27+
(under `s3-auth`) `access_key` and `signature_ttl`; plus the shared
28+
`request_config_setters!(request)`.
2829
There is **no** `auth_token` setter on this builder (the deprecated no-op was removed);
2930
the credential setter is `access_key`.
3031
- `Update` / `UpdateBuilder` (`s3.rs:359`, `s3.rs:247`): the `ReleaseUpdate`
3132
implementation. `Update::configure` returns an `UpdateBuilder`.
3233
`build` (`s3.rs:433`) and `build_async` (under `async`, `s3.rs:442`) both return
3334
the concrete `Update` (which is `Send` and exposes the update verbs as inherent
3435
methods, so no trait import is needed). Backend setters mirror the list builder
35-
(`endpoint`, `bucket_name`, `asset_prefix`, `region`, `access_key`); the common
36+
(`endpoint`, `bucket_name`, `asset_prefix`, `asset_key_pattern`, `region`, `access_key`); the common
3637
setters come from `impl_common_builder_setters!(no_auth_token)` (`s3.rs:314`).
3738
As on the list builder, there is **no** `auth_token` setter (the deprecated shim was
3839
removed); use `access_key`.
@@ -119,12 +120,26 @@ with their assets concatenated (`s3.rs:923`); otherwise it pushes a new release.
119120

120121
### Version derivation
121122

122-
A single case-insensitive regex parses object keys (`s3.rs:834`):
123+
By default a single case-insensitive regex parses object keys (`ASSET_KEY_REGEX`):
123124
`(?i)(?P<prefix>.*/)*(?P<name>.+)-[v]{0,1}(?P<version>\d+\.\d+\.\d+)-.+`.
124125
The key must contain a `name-[v]<major>.<minor>.<patch>-<suffix>` shape: `name`
125126
becomes the release name and the dotted triple becomes the version, with any
126-
leading `v` stripped (`s3.rs:874`). Keys lacking this shape produce no release.
127-
Regex construction failure surfaces as `Error::InvalidResponse` (`s3.rs:836`).
127+
leading `v` stripped. Keys lacking this shape produce no release. The default
128+
version group captures only the `major.minor.patch` triple, so a pre-release key
129+
like `mybin-0.1.2-beta-x86_64-...` parses lossily as `0.1.2` (#61).
130+
131+
`asset_key_pattern(impl Into<String>)` on both builders replaces the default
132+
matcher with a user-supplied regex tuned to the bucket's key layout (e.g. one
133+
whose `version` group admits a pre-release segment, so `0.1.2-beta` /
134+
`0.1.2-beta.1` round-trip). The pattern must define `name` and `version` named
135+
capture groups; `compile_asset_key_pattern` compiles and validates it at
136+
`build()`, surfacing a pattern that does not compile or lacks a required group as
137+
`Error::InvalidAssetKeyPattern` (a `#[non_exhaustive]` variant gated on the `s3`
138+
feature, `Display` prefix "ConfigError:", underlying error chained via
139+
`source()`). At parse time a custom pattern's captured version (after the same
140+
leading-`v` trim) must parse as semver or the key is skipped like a non-matching
141+
key; the default pattern is exempt from that check since its version group only
142+
matches a numeric triple. When unset, behavior is unchanged.
128143

129144
`ReleaseUpdate` selection helpers operate on the parsed list: `pick_latest`
130145
(`s3.rs:498`) picks the highest version (ignoring unparseable ones, erroring
@@ -184,9 +199,9 @@ Missing region (for the region-requiring endpoints) and missing bucket are both
184199
`S3DualStack`, `GCS`, `DigitalOceanSpaces`, `Generic(String)`; plus
185200
`From<&str>` / `From<String>` -> `Generic`.
186201
- `s3::ReleaseList`, `s3::ReleaseListBuilder` (setters: `bucket_name`,
187-
`asset_prefix`, `region`, `endpoint`, `filter_target`, `max_keys`,
188-
`access_key` / `signature_ttl` [s3-auth], request-config setters, `build`);
189-
`ReleaseList::fetch` and `fetch_async` [async].
202+
`asset_prefix`, `asset_key_pattern`, `region`, `endpoint`, `filter_target`,
203+
`max_keys`, `access_key` / `signature_ttl` [s3-auth], request-config setters,
204+
`build`); `ReleaseList::fetch` and `fetch_async` [async].
190205
- `s3::UpdateBuilder`, `s3::Update` (`#[non_exhaustive]`); `Update::configure`,
191206
`build` -> `Update`, `build_async` -> `Update` [async]. `Update` is `Send` with
192207
the inherent verbs (`update`, `update_extended`, `get_latest_release`,
@@ -206,8 +221,13 @@ Missing region (for the region-requiring endpoints) and missing bucket are both
206221
`s3-auth` each continuation URL is freshly signed; `signature_ttl` sets the `X-Amz-Expires`.
207222
- `asset_prefix` appended as `&prefix=<value>`; absent when unset.
208223
- Asset `name` is the key's filename component, not the full key path.
209-
- Version regex requires a `\d+\.\d+\.\d+` triple; leading `v` stripped; keys not
210-
matching produce no release.
224+
- Default version regex requires a `\d+\.\d+\.\d+` triple; leading `v` stripped;
225+
keys not matching produce no release. A pre-release key parses lossily as the
226+
bare triple (`0.1.2-beta` -> `0.1.2`); that default is pinned.
227+
- `asset_key_pattern` replaces the default matcher on both builders; it must
228+
define `name` and `version` named groups, is validated at `build()`
229+
(`Error::InvalidAssetKeyPattern`, source chained), and a custom capture that is
230+
not semver (after the leading-`v` trim) skips the key.
211231
- Releases with the same `name`+`version` merge their assets; empty name/version
212232
dropped.
213233
- Non-2xx listing response is an `Err`, never `Ok` from the error body.
@@ -222,7 +242,11 @@ Missing region (for the region-requiring endpoints) and missing bucket are both
222242

223243
In-module tests (`s3.rs:938`): `parse_s3_response` cases (single/multi asset,
224244
v-prefix strip, multiple releases, non-matching-key skip, path-stripped filename,
225-
malformed-XML error, empty body); `add_to_releases_list` empty-name/version drop;
245+
malformed-XML error, empty body); custom `asset_key_pattern` cases (pre-release
246+
kept and merged, non-semver capture skipped, default lossy pre-release pinned,
247+
bad-pattern / missing-group `build()` errors on both builders, end-to-end
248+
threading through `Update::get_latest_release` and `ReleaseList::fetch` over the
249+
loopback stub); `add_to_releases_list` empty-name/version drop;
226250
loopback-TCP stub tests for the sync and async `ReleaseUpdate` fetch methods
227251
(`get_latest_release`, `get_newer_releases`, `get_release_version`,
228252
`is_update_available`, multi-asset merge); the non-2xx error contract

0 commit comments

Comments
 (0)