mirror of
https://github.com/edu4rdshl/Strata.git
synced 2026-07-18 07:34:45 +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
|
|
@ -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