forked from 1b-agent/opencadstudio-upstream
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:
3 files changed
+124
-19
No files matched your search
@@ -131,6 +131,24 @@ struct Operation {
|
|||||||
fn failure(code: &str, error: impl ToString) -> Value {
|
fn failure(code: &str, error: impl ToString) -> Value {
|
||||||
json!({"ok":false,"status":"failed","code":code,"error":error.to_string()})
|
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> {
|
fn string<'a>(req: &'a Value, key: &str) -> Result<&'a str, Value> {
|
||||||
req[key]
|
req[key]
|
||||||
.as_str()
|
.as_str()
|
||||||
@@ -703,6 +721,24 @@ impl OpenCADStudio {
|
|||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
Ok(match req["op"].as_str().unwrap_or("") {
|
Ok(match req["op"].as_str().unwrap_or("") {
|
||||||
"new" => self.update(Message::TabNew),
|
"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(
|
"open" => self.update(Message::OpenExternal(std::path::PathBuf::from(string(
|
||||||
req, "path",
|
req, "path",
|
||||||
)?))),
|
)?))),
|
||||||
@@ -1409,6 +1445,41 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(path);
|
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]
|
#[test]
|
||||||
fn embed_image_op_places_an_ole2frame_and_undoes_it() {
|
fn embed_image_op_places_an_ole2frame_and_undoes_it() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|||||||
+41
-19
@@ -1182,31 +1182,53 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
// `FileOpened` only installs the result when an open is in
|
// The browser picker + parse happen inside `pick_and_load_web`;
|
||||||
// progress, so mark one. The browser picker + parse happen
|
// the real name is unknown until then, so show a generic label
|
||||||
// inside `pick_and_load_web`; the real name is unknown until
|
// meanwhile.
|
||||||
// then, so show a generic label meanwhile.
|
let (open_id, state) = self.begin_web_open("Opening…".into(), 0);
|
||||||
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,
|
|
||||||
});
|
|
||||||
Task::perform(crate::io::pick_and_load_web(state), move |outcome| {
|
Task::perform(crate::io::pick_and_load_web(state), move |outcome| {
|
||||||
Message::WebFileOpened(open_id, 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 {
|
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 = self.open_job_serial.wrapping_add(1).max(1);
|
||||||
self.open_job_serial
|
self.open_job_serial
|
||||||
|
|||||||
@@ -526,6 +526,18 @@ pub async fn pick_and_load_web(
|
|||||||
let name = handle.file_name();
|
let name = handle.file_name();
|
||||||
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||||
let bytes: Arc<[u8]> = Arc::from(handle.read().await);
|
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 size_bytes = bytes.len() as u64;
|
||||||
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
|
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
|
||||||
let keep_for_recovery = result
|
let keep_for_recovery = result
|
||||||
|
|||||||
Reference in new issue
Block a user