use std::path::PathBuf;
use serde_json::Value;
use super::{
clipboard_temp_dir, named_board_refusal, take_item_from_with, write, Board, ClipboardContent,
ClipboardOptions, ClipboardPolicy,
};
use crate::broker::PathBroker;
pub fn control_args(raw: &str) -> String {
serde_json::from_str::<Value>(raw)
.ok()
.and_then(|v| v.get("args").and_then(|a| a.as_str()).map(str::to_owned))
.unwrap_or_else(|| raw.to_owned())
}
pub fn paste_from_board(broker: &PathBroker, app_id: &str, raw: &str) -> Result<String, String> {
let name = control_args(raw);
if let Some(why) = named_board_refusal(&name) {
return Err(format!("paste_from_board refused {name:?}: {why}"));
}
let item = take_item_from_with(
&Board::Named(name),
broker,
&clipboard_temp_dir(app_id),
ClipboardPolicy::default(),
&ClipboardOptions::default(),
)
.map_err(|e| e.message().to_string())?;
serde_json::to_string(&item).map_err(|e| e.to_string())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StagePlan {
pub board: String,
pub file: Option<String>,
pub image: bool,
}
pub fn plan_stage(raw: &str, prefix: &str) -> Result<StagePlan, String> {
if prefix.trim().is_empty() {
return Err("stage_board needs a non-empty QA board prefix".into());
}
let args = control_args(raw);
let v: Value = serde_json::from_str(&args).map_err(|e| e.to_string())?;
let board = v
.get("board")
.and_then(Value::as_str)
.filter(|n| n.starts_with(prefix))
.ok_or_else(|| {
format!("stage_board needs a board name starting with {prefix}; got {args:.200}")
})?;
if let Some(why) = named_board_refusal(board) {
return Err(format!("stage_board refused {board:?}: {why}"));
}
Ok(StagePlan {
board: board.to_owned(),
file: v.get("file").and_then(Value::as_str).map(str::to_owned),
image: v.get("image").and_then(Value::as_bool).unwrap_or(false),
})
}
pub fn stage_board(raw: &str, prefix: &str) -> Result<String, String> {
let plan = plan_stage(raw, prefix)?;
let mut content = ClipboardContent::default();
if let Some(file) = &plan.file {
content.files.push(file.clone());
}
if plan.image {
content.png = Some(test_png());
}
write(&Board::Named(plan.board.clone()), &content)?;
Ok(serde_json::json!({ "staged": plan.board }).to_string())
}
pub fn temp_dir(app_id: &str) -> PathBuf {
clipboard_temp_dir(app_id)
}
pub fn test_png() -> Vec<u8> {
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &b in data {
crc ^= u32::from(b);
for _ in 0..8 {
crc = if crc & 1 != 0 {
(crc >> 1) ^ 0xEDB8_8320
} else {
crc >> 1
};
}
}
!crc
}
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
let mut body = kind.to_vec();
body.extend_from_slice(data);
out.extend_from_slice(&body);
out.extend_from_slice(&crc32(&body).to_be_bytes());
}
let mut out = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
chunk(&mut out, b"IHDR", &[0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0]);
let raw = [0u8, 0x20, 0x40, 0x80, 0xFF];
let (mut a, mut b) = (1u32, 0u32);
for &x in &raw {
a = (a + u32::from(x)) % 65521;
b = (b + a) % 65521;
}
let mut z = vec![
0x78,
0x01,
0x01,
raw.len() as u8,
0,
!(raw.len() as u8),
0xFF,
];
z.extend_from_slice(&raw);
z.extend_from_slice(&((b << 16) | a).to_be_bytes());
chunk(&mut out, b"IDAT", &z);
chunk(&mut out, b"IEND", &[]);
out
}
#[cfg(test)]
mod tests {
use super::*;
const PREFIX: &str = "rightkit-qa-board-";
#[test]
fn control_args_unwraps_the_request_or_passes_the_input_through() {
assert_eq!(control_args(r#"{"name":"x","args":"board-1"}"#), "board-1");
assert_eq!(control_args("bare-name"), "bare-name");
assert_eq!(control_args(r#"{"name":"x"}"#), r#"{"name":"x"}"#);
}
#[test]
fn paste_refuses_system_and_empty_names_before_touching_a_pasteboard() {
let broker = PathBroker::new();
for raw in [
"Apple CFPasteboard general",
" apple cfpasteboard find",
r#"{"name":"paste_from_board","args":"Apple CFPasteboard drag"}"#,
"",
" ",
] {
let err = paste_from_board(&broker, "rk-qa-refusal-unused", raw).unwrap_err();
assert!(err.contains("refused"), "{raw:?}: {err}");
}
assert!(!temp_dir("rk-qa-refusal-unused").exists());
}
#[test]
fn staging_needs_the_prefix_and_refuses_system_names() {
let ok = plan_stage(
r#"{"board":"rightkit-qa-board-1","file":"/tmp/a.txt","image":true}"#,
PREFIX,
)
.unwrap();
assert_eq!(
ok,
StagePlan {
board: "rightkit-qa-board-1".into(),
file: Some("/tmp/a.txt".into()),
image: true,
}
);
let wrapped = serde_json::json!({
"name": "stage_board",
"args": r#"{"board":"rightkit-qa-board-2"}"#,
})
.to_string();
let wrapped = plan_stage(&wrapped, PREFIX).unwrap();
assert_eq!(wrapped.board, "rightkit-qa-board-2");
assert!(!wrapped.image && wrapped.file.is_none());
assert!(plan_stage(r#"{"board":"other-board"}"#, PREFIX).is_err());
assert!(plan_stage(r#"{"board":"Apple CFPasteboard general"}"#, PREFIX).is_err());
assert!(plan_stage(
r#"{"board":"Apple CFPasteboard general"}"#,
"Apple CFPasteboard"
)
.is_err());
assert!(plan_stage(r#"{"board":"x"}"#, "").is_err());
assert!(plan_stage("not json", PREFIX).is_err());
}
#[test]
fn the_test_png_is_well_formed() {
let png = test_png();
assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]);
assert_eq!(&png[8..16], &[0, 0, 0, 13, b'I', b'H', b'D', b'R']);
assert_eq!(&png[29..33], &[0x1F, 0x15, 0xC4, 0x89]);
assert_eq!(&png[png.len() - 8..png.len() - 4], b"IEND");
}
}