mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #842 from Ni-zav/fix/817-script-binary-file-io-s20261011T141200Z-fd2e
fix(script): preserve raw bytes in BINARY File reads, writes, and copies
This commit is contained in:
commit
80febbd418
3 files changed
+82
-15
No files matched your search
@@ -179,7 +179,7 @@ File.prototype = {
|
||||
get path() { var p = this.__path.split(/[\/\\]/); p.pop(); return p.join("/"); },
|
||||
get parent() { return new Folder(this.path); },
|
||||
get exists() { return JSON.parse(__file("exists", this.__path)) === true; },
|
||||
get length() { return this.__buf !== null ? this.__buf.length : (this.exists ? JSON.parse(__file("read", this.__path)).length : 0); },
|
||||
get length() { return this.__buf !== null ? this.__buf.length : (this.exists ? JSON.parse(__file(this.encoding.toUpperCase() === "BINARY" ? "readBinary" : "read", this.__path)).length : 0); },
|
||||
get eof() { return this.__buf === null || this.__pos >= this.__buf.length; },
|
||||
toString: function () { return this.__path; },
|
||||
open: function (mode) {
|
||||
@@ -193,9 +193,9 @@ File.prototype = {
|
||||
this.__buf = "";
|
||||
return true;
|
||||
}
|
||||
this.__buf = JSON.parse(__file("read", this.__path));
|
||||
this.__buf = JSON.parse(__file(this.encoding.toUpperCase() === "BINARY" ? "readBinary" : "read", this.__path));
|
||||
} else if (mode === "a") {
|
||||
this.__buf = this.exists ? JSON.parse(__file("read", this.__path)) : "";
|
||||
this.__buf = this.exists ? JSON.parse(__file(this.encoding.toUpperCase() === "BINARY" ? "readBinary" : "read", this.__path)) : "";
|
||||
this.__pos = this.__buf.length;
|
||||
} else {
|
||||
// Check the gate now, like After Effects refusing to open a file for writing.
|
||||
@@ -232,18 +232,14 @@ File.prototype = {
|
||||
tell: function () { return this.__pos; },
|
||||
close: function () {
|
||||
if ((this.__mode === "w" || this.__mode === "a" || this.__mode === "e") && this.__buf !== null) {
|
||||
__file("write", this.__path, this.__buf);
|
||||
__file(this.encoding.toUpperCase() === "BINARY" ? "writeBinary" : "write", this.__path, this.__buf);
|
||||
}
|
||||
this.__buf = null;
|
||||
this.__mode = null;
|
||||
return true;
|
||||
},
|
||||
remove: function () { return JSON.parse(__file("remove", this.__path)); },
|
||||
copy: function (to) {
|
||||
var data = JSON.parse(__file("read", this.__path));
|
||||
__file("write", __str(to), data);
|
||||
return true;
|
||||
},
|
||||
copy: function (to) { return JSON.parse(__file("copy", this.__path, __str(to))); },
|
||||
rename: function () { throw __err("File.rename is not supported"); },
|
||||
execute: function () { return JSON.parse(__file("execute", this.__path)); },
|
||||
openDlg: function () { return null; },
|
||||
|
||||
@@ -346,8 +346,16 @@ fn check_access(s: &Session, path: &str, write: bool) -> Result<std::path::PathB
|
||||
Err(format!("Unable to access {path}: scripts can only read files in the project's folder unless {GATE} is on"))
|
||||
}
|
||||
|
||||
/// `__file(op, path, data)`: `read` → text, `write`/`append` → bytes, `exists` → bool, `list` →
|
||||
/// names, `mkdir`, `remove`, `isDir`.
|
||||
/// Encode a binary script string as bytes, without silently corrupting code points outside
|
||||
/// the one-byte range supported by ExtendScript's BINARY encoding.
|
||||
fn binary_bytes(data: &str) -> Result<Vec<u8>, String> {
|
||||
data.chars()
|
||||
.map(|c| u8::try_from(c as u32).map_err(|_| format!("BINARY encoding cannot represent U+{:04X}", c as u32)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `__file(op, path, data)`: text and one-byte-per-character BINARY reads/writes,
|
||||
/// byte-for-byte copy, plus file and folder operations.
|
||||
fn native_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let op = arg_string(args, 0, ctx)?;
|
||||
let path = arg_string(args, 1, ctx)?;
|
||||
@@ -366,19 +374,36 @@ fn native_file(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsV
|
||||
let Ok(p) = check_access(s, &path, false) else { return Ok(json!(false)) };
|
||||
Ok(json!(if op == "exists" { p.exists() } else { p.is_dir() }))
|
||||
}
|
||||
"read" => {
|
||||
"read" | "readBinary" => {
|
||||
let p = check_access(s, &path, false)?;
|
||||
let bytes = s.services.read_file(&p.to_string_lossy()).map_err(|e| format!("Unable to read {path}: {e}"))?;
|
||||
Ok(json!(String::from_utf8_lossy(&bytes)))
|
||||
if op == "readBinary" {
|
||||
// Each byte becomes a single JS character (U+0000..U+00FF).
|
||||
Ok(json!(bytes.into_iter().map(char::from).collect::<String>()))
|
||||
} else {
|
||||
Ok(json!(String::from_utf8_lossy(&bytes)))
|
||||
}
|
||||
}
|
||||
"write" | "append" => {
|
||||
"write" | "writeBinary" | "append" => {
|
||||
let p = check_access(s, &path, true)?;
|
||||
let ps = p.to_string_lossy().to_string();
|
||||
let mut bytes = if op == "append" { s.services.read_file(&ps).unwrap_or_default() } else { vec![] };
|
||||
bytes.extend_from_slice(data.as_bytes());
|
||||
if op == "writeBinary" {
|
||||
bytes.extend(binary_bytes(&data)?);
|
||||
} else {
|
||||
bytes.extend_from_slice(data.as_bytes());
|
||||
}
|
||||
s.services.write_file(&ps, &bytes).map_err(|e| format!("Unable to write {path}: {e}"))?;
|
||||
Ok(json!(bytes.len()))
|
||||
}
|
||||
"copy" => {
|
||||
let src = check_access(s, &path, false)?;
|
||||
let dst = check_access(s, &data, true)?;
|
||||
// Copy the original bytes, not UTF-8-decoded script text.
|
||||
let bytes = s.services.read_file(&src.to_string_lossy()).map_err(|e| format!("Unable to read {path}: {e}"))?;
|
||||
s.services.write_file(&dst.to_string_lossy(), &bytes).map_err(|e| format!("Unable to write {data}: {e}"))?;
|
||||
Ok(json!(true))
|
||||
}
|
||||
"list" => {
|
||||
let p = check_access(s, &path, false)?;
|
||||
let mut names: Vec<String> = std::fs::read_dir(&p)
|
||||
|
||||
@@ -730,3 +730,49 @@ fn import_options_sequence_picks_one_still_or_the_run() {
|
||||
assert_eq!(kind_of(&s, "shot_[0001-0003].png"), FootageKind::Sequence);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn binary_file_io_preserves_all_byte_values() {
|
||||
let dir = std::env::temp_dir().join(format!("ec-script-binary-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let src = dir.join("sig.bin");
|
||||
let dst = dir.join("written.bin");
|
||||
let copied = dir.join("copied.bin");
|
||||
let original = [0x89u8, 0x50, 0x4e, 0x47, 0x00, 0x7f, 0x80, 0xff, 0x0a];
|
||||
std::fs::write(&src, original).unwrap();
|
||||
let mut s = session();
|
||||
s.path = Some(dir.join("project.ecproj").to_string_lossy().to_string());
|
||||
s.prefs.scripting.allow_scripts_write_files = true;
|
||||
let q = |p: &std::path::Path| serde_json::to_string(&p.to_string_lossy()).unwrap();
|
||||
|
||||
let read = ok(&mut s, &format!(
|
||||
"var f = new File({}); f.encoding = 'BINARY'; f.open('r'); var data = f.read(); f.close(); [data.length, data.charCodeAt(0), data.charCodeAt(6), data.charCodeAt(7)]",
|
||||
q(&src)
|
||||
));
|
||||
assert_eq!(read.result, json!([9, 0x89, 0x80, 0xff]));
|
||||
|
||||
ok(&mut s, &format!(
|
||||
"var f = new File({}); f.encoding = 'BINARY'; f.open('w'); f.write(String.fromCharCode(0x89, 0x50, 0x4e, 0x47, 0, 127, 128, 255, 10)); f.close();",
|
||||
q(&dst)
|
||||
));
|
||||
assert_eq!(std::fs::read(&dst).unwrap(), original);
|
||||
|
||||
let copy = ok(&mut s, &format!("new File({}).copy({})", q(&src), q(&copied)));
|
||||
assert_eq!(copy.result, json!(true));
|
||||
assert_eq!(std::fs::read(&copied).unwrap(), original);
|
||||
|
||||
ok(&mut s, &format!(
|
||||
"var f = new File({}); f.encoding = 'BINARY'; f.open('a'); f.write(String.fromCharCode(0xfe)); f.close();",
|
||||
q(&dst)
|
||||
));
|
||||
assert_eq!(std::fs::read(&dst).unwrap(), [original.as_slice(), &[0xfe]].concat());
|
||||
|
||||
let invalid = run_code(
|
||||
&mut s,
|
||||
&format!("var f = new File({}); f.encoding = 'BINARY'; f.open('w'); f.write(String.fromCharCode(0x100)); f.close();", q(&dst)),
|
||||
"invalid-binary.jsx",
|
||||
);
|
||||
assert!(invalid.error.is_some(), "code points above 255 must not be truncated");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user