Web control: open a drawing from bytes

On the web build, {"op":"open","path":…} resolves the name against the
browser's recent-files cache, so a file that is not already there cannot be
opened by the control channel. Accept {"op":"open","name":…,
"data_base64":…} instead: the bytes go through the same parse, caching
and recovery path as a file chosen in the browser picker
(io::open_bytes_web, now shared with pick_and_load_web).

Native builds keep path-based open and reject data_base64 with
invalid_request.

Refs #1349
This commit is contained in:
Helen Kwok committed 2026-09-18 16:43:29 +09:30
1 parent 472bb9883d
commit 2a78ebe2c1
3 files changed
+124 -19

No files matched your search

+71
View File
@@ -131,6 +131,24 @@ struct Operation {
fn failure(code: &str, error: impl ToString) -> Value {
json!({"ok":false,"status":"failed","code":code,"error":error.to_string()})
}
/// `{"op":"open","name":"plan.dwg","data_base64":"…"}`: the web build has no
/// filesystem path to open, so the caller sends the file itself. `name` is
/// reduced to its file name; it labels the tab and the recent-files entry.
fn open_bytes_request(req: &Value) -> Result<(String, Vec<u8>), Value> {
use base64::Engine as _;
let name = string(req, "name")?;
let name = name.rsplit(['/', '\\']).next().unwrap_or(name);
if name.is_empty() {
return Err(failure("invalid_request", "Missing name"));
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(req["data_base64"].as_str().unwrap_or_default())
.map_err(|e| failure("invalid_request", format!("data_base64: {e}")))?;
if bytes.is_empty() {
return Err(failure("invalid_request", "data_base64 is empty"));
}
Ok((name.to_string(), bytes))
}
fn string<'a>(req: &'a Value, key: &str) -> Result<&'a str, Value> {
req[key]
.as_str()
@@ -703,6 +721,24 @@ impl OpenCADStudio {
let i = self.active_tab;
Ok(match req["op"].as_str().unwrap_or("") {
"new" => self.update(Message::TabNew),
"open" if req.get("data_base64").is_some() => {
let (name, bytes) = open_bytes_request(req)?;
#[cfg(target_arch = "wasm32")]
{
if self.opening.is_some() {
return Err(failure("busy", "Another drawing is still opening"));
}
self.open_web_bytes(name, bytes)
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (name, bytes);
return Err(failure(
"invalid_request",
"data_base64 is for the web build; supply path",
));
}
}
"open" => self.update(Message::OpenExternal(std::path::PathBuf::from(string(
req, "path",
)?))),
@@ -1409,6 +1445,41 @@ mod tests {
let _ = std::fs::remove_file(path);
}
#[test]
fn open_from_bytes_validates_the_request() {
let ok = open_bytes_request(&json!({"name":"C:\\plans\\plan.dwg","data_base64":"AAEC"}));
assert_eq!(ok, Ok(("plan.dwg".to_string(), vec![0, 1, 2])));
for (req, error) in [
(json!({"data_base64":"AAEC"}), "Missing name"),
(json!({"name":"dir/","data_base64":"AAEC"}), "Missing name"),
(
json!({"name":"a.dwg","data_base64":""}),
"data_base64 is empty",
),
] {
assert_eq!(
open_bytes_request(&req).unwrap_err()["error"],
error,
"{req}"
);
}
let bad = open_bytes_request(&json!({"name":"a.dwg","data_base64":"not base64!"}));
assert_eq!(bad.unwrap_err()["code"], "invalid_request");
}
#[test]
fn open_from_bytes_is_refused_natively_without_touching_documents() {
let mut app = OpenCADStudio::new_for_test();
request(&mut app, json!({"op":"new"}));
let tabs = app.tabs.len();
let reply = request(
&mut app,
json!({"op":"open","name":"a.dxf","data_base64":"AAEC"}),
);
assert_eq!(reply["code"], "invalid_request", "{reply}");
assert_eq!(app.tabs.len(), tabs);
assert!(app.opening.is_none());
}
#[test]
fn embed_image_op_places_an_ole2frame_and_undoes_it() {
let mut app = OpenCADStudio::new_for_test();
+41 -19
View File
@@ -1182,31 +1182,53 @@ impl OpenCADStudio {
}
#[cfg(target_arch = "wasm32")]
{
// `FileOpened` only installs the result when an open is in
// progress, so mark one. The browser picker + parse happen
// inside `pick_and_load_web`; the real name is unknown until
// then, so show a generic label meanwhile.
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
crate::app::OPEN_PHASE_READING,
));
let open_id = self.next_open_id();
self.opening = Some(crate::app::OpenProgress {
id: open_id,
name: "Opening…".into(),
source_path: None,
size_bytes: 0,
state: state.clone(),
started: Instant::now(),
recovery_error: None,
recovery_read_stats: None,
recovery_bytes: None,
});
// The browser picker + parse happen inside `pick_and_load_web`;
// the real name is unknown until then, so show a generic label
// meanwhile.
let (open_id, state) = self.begin_web_open("Opening…".into(), 0);
Task::perform(crate::io::pick_and_load_web(state), move |outcome| {
Message::WebFileOpened(open_id, outcome)
})
}
}
/// Web: open a drawing from bytes the caller already holds, through the
/// same path as a file chosen in the browser picker.
#[cfg(target_arch = "wasm32")]
pub(in crate::app) fn open_web_bytes(&mut self, name: String, bytes: Vec<u8>) -> Task<Message> {
let (open_id, state) = self.begin_web_open(name.clone(), bytes.len() as u64);
Task::perform(
crate::io::open_bytes_web(name, std::sync::Arc::from(bytes), state),
move |outcome| Message::WebFileOpened(open_id, outcome),
)
}
/// `FileOpened` only installs the result when an open is in progress, so
/// mark one.
#[cfg(target_arch = "wasm32")]
fn begin_web_open(
&mut self,
name: String,
size_bytes: u64,
) -> (u64, std::sync::Arc<crate::io::OpenProgressState>) {
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
crate::app::OPEN_PHASE_READING,
));
let open_id = self.next_open_id();
self.opening = Some(crate::app::OpenProgress {
id: open_id,
name,
source_path: None,
size_bytes,
state: state.clone(),
started: Instant::now(),
recovery_error: None,
recovery_read_stats: None,
recovery_bytes: None,
});
(open_id, state)
}
pub(in crate::app) fn next_open_id(&mut self) -> u64 {
self.open_job_serial = self.open_job_serial.wrapping_add(1).max(1);
self.open_job_serial
+12
View File
@@ -526,6 +526,18 @@ pub async fn pick_and_load_web(
let name = handle.file_name();
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
let bytes: Arc<[u8]> = Arc::from(handle.read().await);
open_bytes_web(name, bytes, progress).await
}
/// Web file open from bytes the caller already holds (the control channel's
/// `open` with `data_base64`). Same parse, caching and recovery handling as a
/// file chosen in the browser picker.
#[cfg(target_arch = "wasm32")]
pub async fn open_bytes_web(
name: String,
bytes: Arc<[u8]>,
progress: Arc<OpenProgressState>,
) -> WebOpenOutcome {
let size_bytes = bytes.len() as u64;
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
let keep_for_recovery = result