mirror of
https://github.com/edu4rdshl/Strata.git
synced 2026-07-17 23:24:46 +00:00
fix: daemon image decoding, preview alignment, ordering and lifecycle
Audit fixes for the daemon:
- Decode gif/webp/bmp/tiff/ico (enable the pure-Rust image features). These
mimes were accepted by pick_mime but the image crate only had png/jpeg, so
thumbnail decode failed and the item was silently dropped. Drop avif (needs
heavy C deps) and svg (not a raster format the image crate decodes) from the
allowlist so unsupported types are never selected.
- ItemAdded signal preview now uses db::PREVIEW_CHARS (200), matching the
history/search queries, so a just-copied row and the same row after a reload
carry identical text.
- Add a deterministic ", rowid DESC" tiebreaker to the three created_at DESC
orderings (history, search, prune) so items sharing a millisecond have a
stable order and prune can't evict the wrong one at the boundary.
- SetConfig now prunes immediately (and emits ItemDeleted) when max_history is
lowered, instead of waiting for the next copy.
- Request the bus name with AllowReplacement (plus ReplaceExisting) so a
reload's new instance can cleanly take the name; fix the misleading comment.
- Wayland monitor clears its pending-offers map on Selection{None} so it can't
grow across copy-then-clear cycles (wlroots path).
- Document that SubmitItem is a trusted-caller method (the password-manager
hint is enforced client-side; forwarding it is a future contract change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
46ce549b2f
commit
e7b0392d92
6 changed files with 125 additions and 14 deletions
78
strata-daemon/Cargo.lock
generated
78
strata-daemon/Cargo.lock
generated
|
|
@ -289,6 +289,12 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
|
|
@ -337,6 +343,12 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -466,6 +478,12 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fax"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
|
|
@ -589,6 +607,26 @@ dependencies = [
|
|||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
|
|
@ -654,13 +692,27 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
|||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
|
|
@ -1010,6 +1062,12 @@ version = "0.1.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.4"
|
||||
|
|
@ -1365,6 +1423,20 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
|
|
@ -1727,6 +1799,12 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ serde_json = "1"
|
|||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
# Image thumbnail generation (runs in spawn_blocking — never blocks async runtime)
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp", "tiff", "ico"] }
|
||||
|
||||
# Fast content hashing for deduplication
|
||||
blake3 = "1"
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ impl Dispatch<ExtDataControlDeviceV1, ()> for MonitorState {
|
|||
offer.destroy();
|
||||
}
|
||||
ext_data_control_device_v1::Event::Selection { id: None } => {
|
||||
// null id means "clipboard cleared" - ignore.
|
||||
// null id means "clipboard cleared". Drop any accumulated offers
|
||||
// so the map can't grow across copy-then-clear cycles.
|
||||
state.offers.clear();
|
||||
}
|
||||
ext_data_control_device_v1::Event::Finished => {
|
||||
tracing::warn!("ext-data-control device finished (compositor revoked access)");
|
||||
|
|
@ -184,7 +186,9 @@ impl Dispatch<ZwlrDataControlDeviceV1, ()> for MonitorState {
|
|||
state.commit_selection(oid);
|
||||
offer.destroy();
|
||||
}
|
||||
zwlr_data_control_device_v1::Event::Selection { id: None } => {}
|
||||
zwlr_data_control_device_v1::Event::Selection { id: None } => {
|
||||
state.offers.clear();
|
||||
}
|
||||
|
||||
zwlr_data_control_device_v1::Event::Finished => {
|
||||
tracing::warn!("zwlr-data-control device finished");
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use std::sync::{Mutex, MutexGuard};
|
|||
/// it is fetched on demand via `get_raw_item` / `GetItemContent` for paste-back.
|
||||
/// Keeping the trim in SQL means a page of large text items costs a few KB of
|
||||
/// JSON, not megabytes, and the work stays in the daemon.
|
||||
const PREVIEW_CHARS: usize = 200;
|
||||
pub const PREVIEW_CHARS: usize = 200;
|
||||
|
||||
/// Metadata for a clipboard entry - does NOT include thumbnail bytes.
|
||||
/// Thumbnails are fetched separately via `get_thumbnail` for lazy loading.
|
||||
|
|
@ -185,7 +185,7 @@ impl Db {
|
|||
"SELECT id, mime_type, substr(content_text, 1, {PREVIEW_CHARS}), source_app, created_at,
|
||||
thumbnail_blob IS NOT NULL AS has_thumb
|
||||
FROM clipboard_history
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY created_at DESC, rowid DESC
|
||||
LIMIT ?1 OFFSET ?2"
|
||||
);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
|
@ -233,7 +233,7 @@ impl Db {
|
|||
h.thumbnail_blob IS NOT NULL AS has_thumb
|
||||
FROM clipboard_history h
|
||||
WHERE h.rowid IN (SELECT rowid FROM clipboard_fts WHERE clipboard_fts MATCH ?1)
|
||||
ORDER BY h.created_at DESC
|
||||
ORDER BY h.created_at DESC, h.rowid DESC
|
||||
LIMIT ?2"
|
||||
);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
|
|
@ -320,7 +320,7 @@ impl Db {
|
|||
let mut stmt = conn.prepare(
|
||||
"SELECT id FROM clipboard_history
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM clipboard_history ORDER BY created_at DESC LIMIT ?1
|
||||
SELECT id FROM clipboard_history ORDER BY created_at DESC, rowid DESC LIMIT ?1
|
||||
)",
|
||||
)?;
|
||||
let ids: Vec<String> = stmt
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ pub struct StrataManager {
|
|||
impl StrataManager {
|
||||
/// Called by the GJS extension when it detects a clipboard change via Meta.Selection.
|
||||
/// `content` is the raw bytes of the clipboard payload (D-Bus `ay`).
|
||||
///
|
||||
/// Trust note: the password-manager hint (`x-kde-passwordManagerHint`) is
|
||||
/// honored client-side by the extension before it ever calls this, but the
|
||||
/// hint is not forwarded, so this method cannot re-check it. Treat callers of
|
||||
/// `SubmitItem` as trusted (the session bus is per-user). Forwarding the
|
||||
/// sensitivity flag to enforce it here is left for a future contract change.
|
||||
async fn submit_item(&self, mime_type: String, content: Vec<u8>) -> zbus::fdo::Result<()> {
|
||||
self.submit_tx
|
||||
.send(SubmitRequest {
|
||||
|
|
@ -205,6 +211,7 @@ impl StrataManager {
|
|||
max_history: u32,
|
||||
max_text_bytes: u32,
|
||||
max_image_bytes: u32,
|
||||
#[zbus(signal_context)] ctx: SignalContext<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
use std::sync::atomic::Ordering;
|
||||
if max_history > 0 {
|
||||
|
|
@ -228,6 +235,23 @@ impl StrataManager {
|
|||
self.limits.max_text_bytes(),
|
||||
self.limits.max_image_bytes(),
|
||||
);
|
||||
|
||||
// Lowering max_history should shrink stored history immediately, not
|
||||
// wait until the next copy triggers a prune. Prune now and emit
|
||||
// ItemDeleted for each removed id so clients can drop their caches.
|
||||
if max_history > 0 {
|
||||
let db = self.db.clone();
|
||||
let max = max_history as usize;
|
||||
let pruned = tokio::task::spawn_blocking(move || db.prune(max))
|
||||
.await
|
||||
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?
|
||||
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))?;
|
||||
for id in &pruned {
|
||||
if let Err(e) = Self::item_deleted(&ctx, id).await {
|
||||
tracing::warn!("Emitting ItemDeleted for pruned id={}: {}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +268,7 @@ impl StrataManager {
|
|||
// -----------------------------------------------------------------
|
||||
|
||||
/// Emitted when a new item is stored (after dedup + thumbnail generation).
|
||||
/// `preview` is a text excerpt (≤120 chars) for text items, or empty for
|
||||
/// `preview` is a text excerpt (≤ PREVIEW_CHARS) for text items, or empty for
|
||||
/// images and other binary types - clients should call GetThumbnail(id) to
|
||||
/// fetch image thumbnails lazily.
|
||||
#[zbus(signal)]
|
||||
|
|
|
|||
|
|
@ -54,12 +54,16 @@ async fn main() -> Result<()> {
|
|||
.await
|
||||
.context("Building D-Bus connection")?;
|
||||
|
||||
// Request the well-known name with ReplaceExisting so a new instance always
|
||||
// wins the race against the outgoing instance during extension reload.
|
||||
// Request the well-known name with ReplaceExisting (try to take it from any
|
||||
// outgoing instance) AND AllowReplacement (let the next instance take it
|
||||
// from us). Both are needed for a clean hand-off on extension reload: without
|
||||
// AllowReplacement on the running instance, a new instance's ReplaceExisting
|
||||
// would be refused.
|
||||
dbus_conn
|
||||
.request_name_with_flags(
|
||||
"org.gnome.Strata",
|
||||
zbus::fdo::RequestNameFlags::ReplaceExisting.into(),
|
||||
zbus::fdo::RequestNameFlags::ReplaceExisting
|
||||
| zbus::fdo::RequestNameFlags::AllowReplacement,
|
||||
)
|
||||
.await
|
||||
.context("Acquiring D-Bus name org.gnome.Strata")?;
|
||||
|
|
@ -152,9 +156,7 @@ fn pick_mime(mimes: &[String]) -> Option<&str> {
|
|||
"image/webp",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/avif",
|
||||
"image/x-icon",
|
||||
"image/svg+xml",
|
||||
// Plain text (UTF-8 preferred, then locale, then X11 legacy aliases).
|
||||
// Ranked above rich-text so editors that expose both text/plain and
|
||||
// text/html give us the raw source, not styled markup.
|
||||
|
|
@ -271,7 +273,10 @@ async fn process_bytes(
|
|||
(None, Some(raw_bytes), Some(thumb_bytes), String::new())
|
||||
} else {
|
||||
let text = String::from_utf8_lossy(&raw_bytes).into_owned();
|
||||
let preview: String = text.chars().take(120).collect();
|
||||
// Same preview length as the paginated/search queries (db::PREVIEW_CHARS),
|
||||
// so a just-copied row and the same row after a reload carry identical
|
||||
// text. `take` is lazy, so this never scans past the first N chars.
|
||||
let preview: String = text.chars().take(db::PREVIEW_CHARS).collect();
|
||||
(Some(text), None, None, preview)
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue