Files
moli/moli-file-api/src/data_transfer.rs
2026-08-11 00:10:12 +08:00

307 lines
9.5 KiB
Rust

const DRAG_MODIFIER_ALT: u8 = 1;
const DRAG_MODIFIER_CTRL: u8 = 2;
const DRAG_MODIFIER_META: u8 = 4;
const DRAG_MODIFIER_SHIFT: u8 = 8;
#[derive(Clone, Copy)]
struct DragModifierState {
alt: bool,
ctrl: bool,
meta: bool,
shift: bool,
}
impl DragModifierState {
fn from_mask(modifiers: u8) -> Self {
Self {
alt: modifiers & DRAG_MODIFIER_ALT != 0,
ctrl: modifiers & DRAG_MODIFIER_CTRL != 0,
meta: modifiers & DRAG_MODIFIER_META != 0,
shift: modifiers & DRAG_MODIFIER_SHIFT != 0,
}
}
}
pub fn normalize_drag_data_type(raw: &str) -> String {
match raw.trim().to_ascii_lowercase().as_str() {
"" => String::new(),
"text" => "text/plain".to_owned(),
"url" => "text/uri-list".to_owned(),
other => other.to_owned(),
}
}
pub fn drag_effect_allowed_from_mask(mask: i32) -> &'static str {
let copy = (mask & 1) != 0;
let link = (mask & 2) != 0;
let r#move = (mask & 16) != 0;
match (copy, link, r#move) {
(false, false, false) => "none",
(true, false, false) => "copy",
(false, true, false) => "link",
(false, false, true) => "move",
(true, true, false) => "copyLink",
(true, false, true) => "copyMove",
(false, true, true) => "linkMove",
(true, true, true) => "all",
}
}
pub fn preferred_drop_effect_from_mask(mask: i32) -> &'static str {
if (mask & 1) != 0 {
"copy"
} else if (mask & 2) != 0 {
"link"
} else if (mask & 16) != 0 {
"move"
} else {
"none"
}
}
pub fn modifier_drop_effect(modifiers: u8) -> Option<&'static str> {
let state = DragModifierState::from_mask(modifiers);
// Match the CDP modifier bitmask policy used by DragEvent construction:
// explicit link gestures win, then copy gestures, then shift-move.
if state.alt || (state.ctrl && state.shift) {
Some("link")
} else if state.ctrl || state.meta {
Some("copy")
} else if state.shift {
Some("move")
} else {
None
}
}
pub fn drop_effect_allowed_by_effect_allowed(
effect_allowed: Option<&str>,
drop_effect: &str,
) -> bool {
let effect_allowed = effect_allowed.unwrap_or("uninitialized");
match drop_effect {
"copy" => matches!(
effect_allowed,
"copy" | "copyLink" | "copyMove" | "all" | "uninitialized"
),
"link" => matches!(
effect_allowed,
"link" | "copyLink" | "linkMove" | "all" | "uninitialized"
),
"move" => matches!(
effect_allowed,
"move" | "copyMove" | "linkMove" | "all" | "uninitialized"
),
_ => false,
}
}
pub fn valid_drop_effect(value: &str) -> bool {
matches!(value, "none" | "copy" | "link" | "move")
}
pub fn valid_effect_allowed(value: &str) -> bool {
matches!(
value,
"none"
| "copy"
| "copyLink"
| "copyMove"
| "link"
| "linkMove"
| "move"
| "all"
| "uninitialized"
)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DataTransferItemSummary {
pub kind: String,
pub item_type: String,
}
impl DataTransferItemSummary {
pub fn new(kind: impl Into<String>, item_type: impl Into<String>) -> Self {
Self {
kind: kind.into(),
item_type: item_type.into(),
}
}
pub fn is_file(&self) -> bool {
self.kind == "file"
}
pub fn is_string_type(&self, item_type: &str) -> bool {
self.kind == "string" && self.item_type == item_type
}
}
pub fn data_transfer_types_from_items(items: &[DataTransferItemSummary]) -> Vec<String> {
let mut types = Vec::new();
let mut has_files = false;
for item in items {
if item.is_file() {
has_files = true;
continue;
}
if item.kind == "string"
&& !item.item_type.is_empty()
&& !types.iter().any(|existing| existing == &item.item_type)
{
types.push(item.item_type.clone());
}
}
if has_files {
types.push("Files".to_owned());
}
types
}
pub fn contains_string_item_type(items: &[DataTransferItemSummary], item_type: &str) -> bool {
items.iter().any(|item| item.is_string_type(item_type))
}
pub fn clear_data_removes_item(item: &DataTransferItemSummary, target_type: Option<&str>) -> bool {
if item.kind != "string" {
return false;
}
target_type.is_none_or(|target_type| item.item_type == target_type)
}
pub fn child_entry_full_path(parent_path: Option<&str>, name: &str) -> String {
match parent_path {
Some(parent) => format!("{}/{}", parent.trim_end_matches('/'), name),
None => format!("/{name}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalizes_legacy_drag_data_type_aliases() {
assert_eq!(normalize_drag_data_type(" text "), "text/plain");
assert_eq!(normalize_drag_data_type("URL"), "text/uri-list");
assert_eq!(
normalize_drag_data_type("Application/JSON"),
"application/json"
);
assert_eq!(normalize_drag_data_type(" "), "");
}
#[test]
fn maps_drag_operation_masks_to_effect_tokens() {
assert_eq!(drag_effect_allowed_from_mask(0), "none");
assert_eq!(drag_effect_allowed_from_mask(1), "copy");
assert_eq!(drag_effect_allowed_from_mask(2), "link");
assert_eq!(drag_effect_allowed_from_mask(16), "move");
assert_eq!(drag_effect_allowed_from_mask(1 | 2), "copyLink");
assert_eq!(drag_effect_allowed_from_mask(1 | 16), "copyMove");
assert_eq!(drag_effect_allowed_from_mask(2 | 16), "linkMove");
assert_eq!(drag_effect_allowed_from_mask(1 | 2 | 16), "all");
assert_eq!(preferred_drop_effect_from_mask(1 | 2 | 16), "copy");
assert_eq!(preferred_drop_effect_from_mask(2 | 16), "link");
assert_eq!(preferred_drop_effect_from_mask(16), "move");
assert_eq!(preferred_drop_effect_from_mask(0), "none");
}
#[test]
fn drag_modifier_drop_effect_maps_cdp_modifier_masks_by_precedence() {
assert_eq!(modifier_drop_effect(0), None);
assert_eq!(modifier_drop_effect(DRAG_MODIFIER_CTRL), Some("copy"));
assert_eq!(modifier_drop_effect(DRAG_MODIFIER_META), Some("copy"));
assert_eq!(modifier_drop_effect(DRAG_MODIFIER_SHIFT), Some("move"));
assert_eq!(modifier_drop_effect(DRAG_MODIFIER_ALT), Some("link"));
assert_eq!(
modifier_drop_effect(DRAG_MODIFIER_CTRL | DRAG_MODIFIER_SHIFT),
Some("link")
);
assert_eq!(
modifier_drop_effect(DRAG_MODIFIER_ALT | DRAG_MODIFIER_META),
Some("link")
);
assert_eq!(
modifier_drop_effect(DRAG_MODIFIER_CTRL | DRAG_MODIFIER_META),
Some("copy")
);
}
#[test]
fn drag_modifier_drop_effect_respects_effect_allowed_filter() {
assert!(drop_effect_allowed_by_effect_allowed(Some("all"), "copy"));
assert!(drop_effect_allowed_by_effect_allowed(Some("all"), "link"));
assert!(drop_effect_allowed_by_effect_allowed(Some("all"), "move"));
assert!(drop_effect_allowed_by_effect_allowed(
Some("copyLink"),
"copy"
));
assert!(drop_effect_allowed_by_effect_allowed(
Some("copyLink"),
"link"
));
assert!(!drop_effect_allowed_by_effect_allowed(
Some("copyLink"),
"move"
));
assert!(!drop_effect_allowed_by_effect_allowed(Some("copy"), "link"));
}
#[test]
fn validates_effect_tokens() {
assert!(valid_drop_effect("copy"));
assert!(valid_drop_effect("none"));
assert!(!valid_drop_effect("copyLink"));
assert!(valid_effect_allowed("copyLink"));
assert!(valid_effect_allowed("uninitialized"));
assert!(!valid_effect_allowed("invalid"));
}
#[test]
fn projects_data_transfer_types_from_item_summaries() {
let items = vec![
DataTransferItemSummary::new("string", "text/plain"),
DataTransferItemSummary::new("string", "text/html"),
DataTransferItemSummary::new("string", "text/plain"),
DataTransferItemSummary::new("string", ""),
DataTransferItemSummary::new("file", "image/png"),
];
assert_eq!(
data_transfer_types_from_items(&items),
vec![
"text/plain".to_owned(),
"text/html".to_owned(),
"Files".to_owned()
]
);
assert!(contains_string_item_type(&items, "text/html"));
assert!(!contains_string_item_type(&items, "image/png"));
}
#[test]
fn clear_data_removal_rule_only_targets_string_items() {
let text = DataTransferItemSummary::new("string", "text/plain");
let html = DataTransferItemSummary::new("string", "text/html");
let file = DataTransferItemSummary::new("file", "text/plain");
assert!(clear_data_removes_item(&text, None));
assert!(clear_data_removes_item(&text, Some("text/plain")));
assert!(!clear_data_removes_item(&html, Some("text/plain")));
assert!(!clear_data_removes_item(&file, None));
}
#[test]
fn builds_child_entry_full_paths() {
assert_eq!(child_entry_full_path(None, "file.txt"), "/file.txt");
assert_eq!(
child_entry_full_path(Some("/parent/"), "child.txt"),
"/parent/child.txt"
);
}
}