Skip to content

Commit ffa11d6

Browse files
committed
fix: to_archive and task threads
fixes to_archive work with buffers as well as files. Settings work on Neon/Node tasks
1 parent 8d420cc commit ffa11d6

File tree

8 files changed

+141
-11
lines changed

8 files changed

+141
-11
lines changed

js-src/Builder.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { isActionsAssertion } from "./assertions.js";
2929
import { CallbackSigner, LocalSigner } from "./Signer.js";
3030
import { Reader } from "./Reader.js";
3131
import { Builder } from "./Builder.js";
32+
import { loadC2paSettings, resetSettings } from "./Settings.js";
3233

3334
const tempDir = path.join(__dirname, "tmp");
3435

@@ -277,6 +278,81 @@ describe("Builder", () => {
277278
expect(activeManifest?.title).toBe("Test_Manifest");
278279
});
279280

281+
it("should populate buffer when archiving to buffer", async () => {
282+
const archive: DestinationBufferAsset = {
283+
buffer: null,
284+
};
285+
await builder.toArchive(archive);
286+
expect(archive.buffer).not.toBeNull();
287+
expect(archive.buffer!.length).toBeGreaterThan(0);
288+
});
289+
290+
it("should write archive to file", async () => {
291+
const archivePath = path.join(tempDir, "archive_file_test.zip");
292+
const archive = { path: archivePath };
293+
await builder.toArchive(archive);
294+
295+
// Verify file was created and has content
296+
expect(await fs.pathExists(archivePath)).toBe(true);
297+
const stats = await fs.stat(archivePath);
298+
expect(stats.size).toBeGreaterThan(0);
299+
});
300+
301+
it("should construct reader directly from builder archive buffer", async () => {
302+
// Enable c2pa archive format
303+
loadC2paSettings(
304+
JSON.stringify({
305+
builder: {
306+
generate_c2pa_archive: true,
307+
},
308+
verify: {
309+
verify_after_reading: false,
310+
},
311+
}),
312+
);
313+
314+
try {
315+
// Create a builder
316+
const simpleManifestDefinition = {
317+
claim_generator_info: [
318+
{
319+
name: "c2pa_test",
320+
version: "1.0.0",
321+
},
322+
],
323+
title: "Test_Manifest",
324+
format: "image/jpeg",
325+
assertions: [],
326+
resources: { resources: {} },
327+
};
328+
329+
const testBuilder = Builder.withJson(simpleManifestDefinition);
330+
331+
// Add an ingredient
332+
await testBuilder.addIngredient(parent_json, source);
333+
334+
// Create an archive from the builder written to a buffer
335+
const archive: DestinationBufferAsset = {
336+
buffer: null,
337+
};
338+
await testBuilder.toArchive(archive);
339+
340+
// Verify buffer was populated
341+
expect(archive.buffer).not.toBeNull();
342+
expect(archive.buffer!.length).toBeGreaterThan(0);
343+
344+
// Construct a reader from the builder archive with mime-type "application/c2pa"
345+
const reader = await Reader.fromAsset({
346+
buffer: archive.buffer! as Buffer,
347+
mimeType: "application/c2pa",
348+
});
349+
expect(reader).not.toBeNull();
350+
} finally {
351+
// Reset settings to defaults
352+
resetSettings();
353+
}
354+
});
355+
280356
it("should sign data with callback to file", async () => {
281357
const dest: FileAsset = {
282358
path: path.join(tempDir, "callback_signed.jpg"),

js-src/Builder.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,8 @@ export class Builder implements BuilderInterface {
235235
value,
236236
);
237237
}
238+
239+
getHandle(): NeonBuilderHandle {
240+
return this.builder;
241+
}
238242
}

js-src/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ export interface BuilderInterface {
315315
* @returns The manifest definition
316316
*/
317317
updateManifestProperty(property: string, value: string | ClaimVersion): void;
318+
319+
/**
320+
* Get the internal handle for use with Neon bindings
321+
*/
322+
getHandle(): NeonBuilderHandle;
318323
}
319324

320325
export interface ReaderInterface {

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ pub enum Error {
5151
#[error("Settings handling failed: {0}")]
5252
Settings(String),
5353

54+
#[error("Reading failed: {0}")]
55+
Reading(String),
56+
5457
#[error("Signing failed: {0}")]
5558
Signing(String),
5659

src/neon_builder.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -235,25 +235,47 @@ impl NeonBuilder {
235235

236236
pub fn to_archive(mut cx: FunctionContext) -> JsResult<JsPromise> {
237237
let this = cx.this::<JsBox<Self>>()?;
238-
let dest = cx
239-
.argument::<JsObject>(0)
240-
.and_then(|obj| parse_asset(&mut cx, obj))?;
238+
let dest_obj = cx.argument::<JsObject>(0)?;
239+
let dest = parse_asset(&mut cx, dest_obj)?;
240+
let is_buffer = dest.name() == "destination_buffer";
241241
let builder = Arc::clone(&this.builder);
242+
let dest_obj_root: Arc<Root<JsObject>> = Arc::new(Root::new(&mut cx, &dest_obj));
242243

243244
let promise = cx
244245
.task(move || {
245246
// Block on acquiring the async mutex lock
247+
// Settings are automatically applied when runtime() is called
246248
let rt = runtime();
247249
let mut builder = rt.block_on(async { builder.lock().await });
248250

249-
dest.write_stream().and_then(|dest_stream| {
250-
builder.to_archive(dest_stream)?;
251-
Ok(())
251+
dest.write_stream().and_then(|mut dest_stream| {
252+
builder.to_archive(&mut dest_stream)?;
253+
if is_buffer {
254+
let mut archive_data = Vec::new();
255+
dest_stream
256+
.rewind()
257+
.map_err(|e| Error::Asset(format!("Failed to rewind stream: {e}")))?;
258+
dest_stream
259+
.read_to_end(&mut archive_data)
260+
.map_err(|e| Error::Asset(format!("Failed to read stream: {e}")))?;
261+
Ok(Some(archive_data))
262+
} else {
263+
Ok(None)
264+
}
252265
})
253266
})
254-
.promise(move |mut cx, result: Result<(), Error>| match result {
255-
Ok(_) => Ok(cx.undefined()),
256-
Err(err) => as_js_error(&mut cx, err).and_then(|err| cx.throw(err)),
267+
.promise(move |mut cx, result: Result<Option<Vec<u8>>, Error>| {
268+
match result {
269+
Ok(Some(archive_data)) => {
270+
// If the output is a buffer, populate it with the archive data
271+
let buffer = JsBuffer::from_slice(&mut cx, &archive_data)?;
272+
let dest_obj = dest_obj_root.to_inner(&mut cx);
273+
dest_obj.set(&mut cx, "buffer", buffer)?;
274+
Ok(cx.undefined())
275+
}
276+
Ok(None) => Ok(cx.undefined()),
277+
Err(err) => as_js_error(&mut cx, err).and_then(|err| cx.throw(err)),
278+
}
257279
});
258280
Ok(promise)
259281
}

src/neon_reader.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl NeonReader {
5252
let format = source
5353
.mime_type()
5454
.ok_or_else(|| {
55-
Error::Signing("Ingredient asset must have a mime type".to_string())
55+
Error::Reading("Source asset must have a mime type".to_string())
5656
})?
5757
.to_owned();
5858

@@ -100,7 +100,7 @@ impl NeonReader {
100100
let format = asset
101101
.mime_type()
102102
.ok_or_else(|| {
103-
Error::Signing("Ingredient asset must have a mime type".to_string())
103+
Error::Reading("Source asset must have a mime type".to_string())
104104
})?
105105
.to_owned();
106106
let stream = asset.into_read_stream()?;

src/runtime.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ fn build_runtime() -> Arc<Runtime> {
2828
}
2929

3030
pub fn runtime() -> Arc<Runtime> {
31+
// Ensure settings are applied to the current thread
32+
// This is important for Neon tasks that don't run on Tokio worker threads
33+
crate::settings::apply_settings_to_current_thread();
34+
3135
let cell = RUNTIME.get_or_init(|| RwLock::new(build_runtime()));
3236
cell.read().unwrap().clone()
3337
}

src/settings.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ pub(crate) fn get_global_settings_toml() -> Option<String> {
3232
global_toml().read().unwrap().clone()
3333
}
3434

35+
/// Apply settings to the current thread from the global TOML snapshot.
36+
/// This should be called whenever settings might be needed, as settings may have changed.
37+
pub(crate) fn apply_settings_to_current_thread() {
38+
if let Some(toml) = get_global_settings_toml() {
39+
let _ = Settings::from_toml(&toml);
40+
}
41+
}
42+
3543
/// Parse a JSON string (argument 0) into c2pa-rs Settings and apply them globally.
3644
/// Returns undefined; the JSON snapshot is stored globally for new worker threads.
3745
pub fn load_settings(mut cx: FunctionContext) -> JsResult<JsUndefined> {
@@ -47,6 +55,8 @@ pub fn load_settings(mut cx: FunctionContext) -> JsResult<JsUndefined> {
4755
.or_else(|e| cx.throw_error(format!("Failed to get settings as TOML: {}", e)))?;
4856
// Save the JSON snapshot for new worker threads
4957
set_global_settings_toml(Some(toml_string));
58+
// Apply settings to current thread
59+
apply_settings_to_current_thread();
5060
reload_runtime();
5161
Ok(cx.undefined())
5262
}
@@ -68,6 +78,8 @@ pub fn load_settings_toml(mut cx: FunctionContext) -> JsResult<JsUndefined> {
6878
.or_else(|e| cx.throw_error(format!("Failed to get settings as TOML: {}", e)))?;
6979
// Save the TOML snapshot for new worker threads
7080
set_global_settings_toml(Some(toml_string));
81+
// Apply settings to current thread
82+
apply_settings_to_current_thread();
7183
reload_runtime();
7284
Ok(cx.undefined())
7385
}
@@ -152,6 +164,7 @@ pub fn load_trust_config(mut cx: FunctionContext) -> JsResult<JsUndefined> {
152164
let full_toml = Settings::to_toml()
153165
.or_else(|e| cx.throw_error(format!("Failed to get settings as TOML: {}", e)))?;
154166
set_global_settings_toml(Some(full_toml));
167+
apply_settings_to_current_thread();
155168
reload_runtime();
156169
Ok(cx.undefined())
157170
}
@@ -202,6 +215,7 @@ pub fn load_cawg_trust_config(mut cx: FunctionContext) -> JsResult<JsUndefined>
202215
let full_toml = Settings::to_toml()
203216
.or_else(|e| cx.throw_error(format!("Failed to get settings as TOML: {}", e)))?;
204217
set_global_settings_toml(Some(full_toml));
218+
apply_settings_to_current_thread();
205219
reload_runtime();
206220
Ok(cx.undefined())
207221
}
@@ -424,6 +438,7 @@ pub fn load_verify_config(mut cx: FunctionContext) -> JsResult<JsUndefined> {
424438
.or_else(|e| cx.throw_error(format!("Failed to get settings as TOML: {}", e)))?;
425439
// Save the TOML snapshot for new worker threads
426440
set_global_settings_toml(Some(toml_string));
441+
apply_settings_to_current_thread();
427442
reload_runtime();
428443
Ok(cx.undefined())
429444
}
@@ -496,6 +511,7 @@ pub fn reset_settings(mut cx: FunctionContext) -> JsResult<JsUndefined> {
496511
Ok(_) => {
497512
// Store the default TOML for worker threads
498513
set_global_settings_toml(Some(default_toml));
514+
apply_settings_to_current_thread();
499515
reload_runtime();
500516
Ok(cx.undefined())
501517
}

0 commit comments

Comments
 (0)