Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions core/src/main/java/io/roastedroot/quickjs4j/core/Engine.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public final class Engine implements AutoCloseable {
private static final int ALIGNMENT = 1;
public static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper();

private final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
private final ByteArrayOutputStream stderr = new ByteArrayOutputStream();
private final ByteArrayOutputStream stdout;
private final ByteArrayOutputStream stderr;
private final WasiOptions wasiOpts;

private final WasiPreview1 wasi;
Expand Down Expand Up @@ -66,10 +66,14 @@ private Engine(
ObjectMapper mapper,
Function<MemoryLimits, Memory> memoryFactory,
ScriptCache cache,
Logger logger) {
Logger logger,
ByteArrayOutputStream stdout,
ByteArrayOutputStream stderr) {
this.mapper = mapper;
this.builtins = builtins;
this.cache = cache;
this.stdout = stdout;
this.stderr = stderr;

// builtins to make invoke dynamic javascript functions
builtins.put(
Expand Down Expand Up @@ -454,7 +458,7 @@ public String stderr() {
try {
stderr.flush();
} catch (IOException ex) {
throw new RuntimeException("Failed to flush stdout");
throw new RuntimeException("Failed to flush stderr");
}

return stderr.toString(UTF_8);
Expand Down Expand Up @@ -533,6 +537,8 @@ public static final class Builder {
private Function<MemoryLimits, Memory> memoryFactory;
private ScriptCache cache;
private Logger logger;
private ByteArrayOutputStream stdout;
private ByteArrayOutputStream stderr;

private Builder() {}

Expand Down Expand Up @@ -566,6 +572,16 @@ public Builder withLogger(Logger logger) {
return this;
}

public Builder withStdout(ByteArrayOutputStream stdout) {
this.stdout = stdout;
return this;
}

public Builder withStderr(ByteArrayOutputStream stderr) {
this.stderr = stderr;
return this;
}

public Engine build() {
if (mapper == null) {
mapper = DEFAULT_OBJECT_MAPPER;
Expand All @@ -589,7 +605,21 @@ public Engine build() {
if (logger == null) {
logger = new SystemLogger();
}
return new Engine(finalBuiltins, finalInvokables, mapper, memoryFactory, cache, logger);
if (stdout == null) {
stdout = new ByteArrayOutputStream();
}
if (stderr == null) {
stderr = new ByteArrayOutputStream();
}
return new Engine(
finalBuiltins,
finalInvokables,
mapper,
memoryFactory,
cache,
logger,
stdout,
stderr);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.roastedroot.quickjs4j.core;

import java.io.ByteArrayOutputStream;

public class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
private final int maxBytes;

public BoundedByteArrayOutputStream(int maxBytes) {
this.maxBytes = maxBytes;
}

@Override
public synchronized void write(int b) {
if (size() >= maxBytes) {
throw new RuntimeException("Output stream exceeded limit of " + maxBytes + " bytes");
}
super.write(b);
}

@Override
public synchronized void write(byte[] b, int off, int len) {
if (size() + len > maxBytes) {
throw new RuntimeException("Output stream exceeded limit of " + maxBytes + " bytes");
}
super.write(b, off, len);
}

@Override
public synchronized void writeBytes(byte[] b) {
if (size() + b.length > maxBytes) {
throw new RuntimeException("Output stream exceeded limit of " + maxBytes + " bytes");
}
super.writeBytes(b);
}
}
26 changes: 26 additions & 0 deletions core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,4 +350,30 @@ public void withExecutorService() {
runner.compileAndExec("console.log('something something');");
}
}

@Test
public void boundedStdoutStopsExecution() throws Exception {
var boundedStdout = new BoundedByteArrayOutputStream(1024);
var es = Executors.newSingleThreadExecutor();
var engine = Engine.builder().withStdout(boundedStdout).build();
var runner = Runner.builder().withEngine(engine).withExecutorService(es).build();

// No timeout — the bounded stream should cause the error
var ex =
assertThrows(
RuntimeException.class,
() ->
runner.compileAndExec(
"while(true) { console.log('x'.repeat(100)); }"));

assertTrue(
ex.getMessage().contains("exceeded limit"),
"Expected stream limit error, got: " + ex.getMessage());

// The executor thread should be free after the stream error
var probe = es.submit(() -> "ok");
assertEquals("ok", probe.get(5, java.util.concurrent.TimeUnit.SECONDS));

runner.close();
}
}
Loading