diff --git a/crates/ipx/CHANGELOG.md b/crates/ipx/CHANGELOG.md index 015a96d..b30a06e 100644 --- a/crates/ipx/CHANGELOG.md +++ b/crates/ipx/CHANGELOG.md @@ -40,10 +40,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Artwork a publisher sends without an image type, or as `application/octet-stream`, is shown when it is a JPEG, PNG, GIF or WebP, instead of initials (#279). - A 429 from TypeSafe holds every call back as it asks, instead of the pass going on asking (#277). - Looking for items to read for Signals read every item on the server on each scan; it now reads only recent ones (#274). +- With the downloads folder out of reach during a clean-up, as when the array stops, every downloaded episode was taken as deleted by hand and forgotten; the clean-up now leaves them alone until the folder is back (#293). +- A feed address that serves something larger than 50 MB, such as an episode's file given as a feed, is refused as too large, instead of read whole into memory at every check (#295). +- Searching items for `%` or `_` finds those characters, instead of treating them as wildcards that matched nearly everything (#296). ### Security - ammonia, which cleans publishers' HTML, updated to 4.2.4 for three advisories, among them XSS through SVG animation tags; rustls to 0.23.46 for five and h2 to 0.4.20 for one, in every connection ipx makes (#281). +- The address of a file being downloaded, which for Patreon carries its signature, is sent to open pages and written to the log without its query, as other addresses already were (#292). ## [1.3.0] - 2026-10-16 diff --git a/crates/ipx/src/db.rs b/crates/ipx/src/db.rs index 22c257f..3a15eff 100644 --- a/crates/ipx/src/db.rs +++ b/crates/ipx/src/db.rs @@ -1222,8 +1222,13 @@ fn entries_from( } let search = match search.map(|q| q.trim().to_lowercase()).filter(|q| !q.is_empty()) { Some(q) => { + // % and _ as typed, not as wildcards: "100%" matched anything starting 100 (#296). + let q = q.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"); let like = a.p(format!("%{q}%")); - format!("(lower(coalesce(e.title, '')) LIKE {like} OR lower(coalesce(e.description, '')) LIKE {like})") + format!( + "(lower(coalesce(e.title, '')) LIKE {like} ESCAPE '\\' \ + OR lower(coalesce(e.description, '')) LIKE {like} ESCAPE '\\')" + ) } None => "true".into(), }; @@ -3614,6 +3619,9 @@ mod tests { assert_eq!(db.count_in(7, Some("f"), Filter::Downloaded, None).await.unwrap(), 1); assert_eq!(db.count_in(7, Some("f"), Filter::Flagged, None).await.unwrap(), 1); assert_eq!(db.count_in(7, Some("f"), Filter::All, Some("dive")).await.unwrap(), 2); + for wild in ["%", "_", "\\"] { + assert_eq!(db.count_in(7, Some("f"), Filter::All, Some(wild)).await.unwrap(), 0, "{wild} is only itself"); + } assert_eq!( db.count_in(7, Some("f"), Filter::All, Some("NOTES two")).await.unwrap(), 1,