replace openpnp-capture-java dependency with ffm-based implementation
What changed, and why it matters
This commit swaps out the library Sparrow Wallet uses to talk to webcams. The old library used JNA (a common Java-to-native bridge), while the new one uses Java's newer Foreign Function & Memory (FFM) API. The change also adds a manual step that tries to load a native library file named openpnp-capture directly from the Java installation directory. There is no direct evidence in the commit of a security vulnerability, but any change that touches native camera access and library loading deserves a careful look because mistakes there can create ways for attackers to run malicious code or bypass sandbox protections.
Treat this as a supply-chain and native-code change worth reviewing. Verify the new io.github.doblon8:openpnp-capture-java artifact is published by a trusted source, inspect its FFM bindings and native binaries for memory-safety issues, and confirm the System.load path cannot be influenced by an attacker (e.g., via JAVA_HOME or a writable java.home). Continue normal dependency monitoring; no immediate patch is required solely based on this diff.
Security signals we found
Dependency swap from JNA-based to FFM-based native bindings for webcam capture
New manual System.load of openpnp-capture native library from java.home/lib
New --enable-native-access flag for io.github.doblon8.openpnp.capture
Native library extraction glob changed to io.github.doblon8.openpnp.capture/native/**
Removal of JNA module shim and com.sun.jna module requirement
Evidence from the diff
The commit replaces org.openpnp:openpnp-capture-java with io.github.doblon8:openpnp-capture-java, an FFM-based reimplementation of the same OpenPnP Capture bindings. It updates package imports, method calls (record-style accessors such as formatInfo().width()), module-info requirements, JVM native-access flags, and native library extraction paths. A notable addition is a static initializer block that calls System.load(java.home/lib/
Changed components
QRScanDialog.javaWebcamService.javaWebcamResolution.javaWebcamView.javabuild.gradle native library extraction and JVM argsmodule-info.javaInspect captured patch +54 / −53
diff --git a/build.gradle b/build.gradle
index 80ea6ca..e4bf1e8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -73,7 +73,7 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
- implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
+ implementation('io.github.doblon8:openpnp-capture-java:0.0.3')
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
@@ -140,6 +140,7 @@ application {
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
+ "--enable-native-access=io.github.doblon8.openpnp.capture",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
@@ -203,10 +204,8 @@ jlink {
'glob:/org.hid4java/darwin-*/**,' +
'glob:/org.hid4java/linux-*/**,' +
'glob:/org.hid4java/win32-*/**,' +
- 'glob:/openpnp.capture.java/darwin-*/**,' +
- 'glob:/openpnp.capture.java/linux-*/**,' +
- 'glob:/openpnp.capture.java/win32-*/**,' +
- 'glob:/io.github.doblon8.jzbar/native/**']
+ 'glob:/io.github.doblon8.jzbar/native/**,' +
+ 'glob:/io.github.doblon8.openpnp.capture/native/**']
launcher {
name = 'sparrow'
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
@@ -216,6 +215,7 @@ jlink {
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
+ "--enable-native-access=io.github.doblon8.openpnp.capture",
"--enable-native-access=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
@@ -394,13 +394,13 @@ def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
def nativeLibJars = [
- 'jna-' : "com/sun/jna/${jnaPlatform}/*",
- 'argon2-jvm-2' : "${jnaPlatform}/*",
- 'hid4java-' : "${jnaPlatform}/*",
- 'openpnp-capture-java': "${jnaPlatform}/*",
- 'jSerialComm-' : "${serialOs}/${serialArch}/*",
- 'usb4java-' : "org/usb4java/${jnaPlatform}/*",
- 'jzbar-' : "native/${osName}/${osArch}/*",
+ 'jna-' : "com/sun/jna/${jnaPlatform}/*",
+ 'argon2-jvm-2' : "${jnaPlatform}/*",
+ 'hid4java-' : "${jnaPlatform}/*",
+ 'jSerialComm-' : "${serialOs}/${serialArch}/*",
+ 'usb4java-' : "org/usb4java/${jnaPlatform}/*",
+ 'jzbar-' : "native/${osName}/${osArch}/*",
+ 'openpnp-capture-java-': "native/${osName}/${osArch}/*",
]
tasks.register('extractNativeLibraries') {
@@ -477,12 +477,6 @@ extraJavaModuleInfo {
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
- module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
- exports('org.openpnp.capture')
- exports('org.openpnp.capture.library')
- requires('java.desktop')
- requires('com.sun.jna')
- }
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
exports('com.csvreader')
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
index 1198b47..7130b89 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
@@ -47,7 +47,7 @@ import javafx.scene.layout.*;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.tools.Borders;
-import org.openpnp.capture.CaptureDevice;
+import io.github.doblon8.openpnp.capture.CaptureDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamResolution.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamResolution.java
index 396606e..6cdd446 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamResolution.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamResolution.java
@@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.control;
-import org.openpnp.capture.CaptureFormat;
+import io.github.doblon8.openpnp.capture.CaptureFormat;
import java.util.Arrays;
@@ -61,7 +61,7 @@ public enum WebcamResolution implements Comparable<WebcamResolution> {
public static WebcamResolution from(CaptureFormat captureFormat) {
for(WebcamResolution resolution : values()) {
- if(captureFormat.getFormatInfo().width == resolution.width && captureFormat.getFormatInfo().height == resolution.height) {
+ if(captureFormat.formatInfo().width() == resolution.width && captureFormat.formatInfo().height() == resolution.height) {
return resolution;
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
index 45e77ca..1c332a0 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
@@ -16,8 +16,7 @@ import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
-import org.openpnp.capture.*;
-import org.openpnp.capture.library.OpenpnpCaptureLibrary;
+import io.github.doblon8.openpnp.capture.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,6 +24,7 @@ import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
+import java.io.File;
import java.util.*;
import java.util.List;
import java.util.concurrent.Semaphore;
@@ -62,38 +62,46 @@ public class WebcamService extends ScheduledService<Image> {
private final Bokmakierie bokmakierie;
static {
+ String javaHome = System.getProperty("java.home");
+ if(javaHome != null) {
+ File libFile = new File(new File(javaHome, "lib"), System.mapLibraryName("openpnp-capture"));
+ if(libFile.exists()) {
+ System.load(libFile.getAbsolutePath());
+ }
+ }
+
if(log.isTraceEnabled()) {
- OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(8);
+ OpenPnpCapture.setLogLevel(LogLevel.VERBOSE);
} else if(log.isDebugEnabled()) {
- OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(7);
+ OpenPnpCapture.setLogLevel(LogLevel.DEBUG);
} else if(log.isInfoEnabled()) {
- OpenpnpCaptureLibrary.INSTANCE.Cap_setLogLevel(6);
+ OpenPnpCapture.setLogLevel(LogLevel.INFO);
}
- OpenpnpCaptureLibrary.INSTANCE.Cap_installCustomLogFunction((level, ptr) -> {
+ OpenPnpCapture.installCustomLogFunction((level, message) -> {
switch(level) {
- case 0:
- case 1:
- case 2:
- case 3:
- String err = ptr.getString(0).trim();
+ case LogLevel.EMERGENCY:
+ case LogLevel.ALERT:
+ case LogLevel.CRITICAL:
+ case LogLevel.ERROR:
+ String err = message.trim();
if(err.equals("tjDecompressHeader2 failed: No error") || err.matches("getPropertyLimits.*failed on.*")) { //Safe to ignore
log.debug(err);
} else {
log.error(err);
}
break;
- case 4:
- case 5:
- case 6:
- log.info(ptr.getString(0).trim());
+ case LogLevel.WARNING:
+ case LogLevel.NOTICE:
+ case LogLevel.INFO:
+ log.info(message.trim());
break;
- case 7:
- log.debug(ptr.getString(0).trim());
+ case LogLevel.DEBUG:
+ log.debug(message.trim());
break;
- case 8:
+ case LogLevel.VERBOSE:
default:
- log.trace(ptr.getString(0).trim());
+ log.trace(message.trim());
break;
}
});
@@ -167,8 +175,8 @@ public class WebcamService extends ScheduledService<Image> {
//On macOS and Windows, camera pixel format is largely abstracted away
if(OsType.getCurrent() == OsType.UNIX) {
deviceFormats.sort((f1, f2) -> {
- WebcamPixelFormat pf1 = WebcamPixelFormat.fromFourCC(f1.getFormatInfo().fourcc);
- WebcamPixelFormat pf2 = WebcamPixelFormat.fromFourCC(f2.getFormatInfo().fourcc);
+ WebcamPixelFormat pf1 = WebcamPixelFormat.fromFourCC(f1.formatInfo().fourcc());
+ WebcamPixelFormat pf2 = WebcamPixelFormat.fromFourCC(f2.formatInfo().fourcc());
return Integer.compare(WebcamPixelFormat.getPriority(pf1), WebcamPixelFormat.getPriority(pf2));
});
}
@@ -185,17 +193,17 @@ public class WebcamService extends ScheduledService<Image> {
format = supportedResolutions.get(resolution);
} else {
format = device.getFormats().getFirst();
- log.warn("Could not get standard capture resolution, using " + format.getFormatInfo().width + "x" + format.getFormatInfo().height);
+ log.warn("Could not get standard capture resolution, using " + format.formatInfo().width() + "x" + format.formatInfo().height());
}
}
//On Linux, formats not defined in WebcamPixelFormat are unsupported
- if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
- log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
+ if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.formatInfo().fourcc()) == null) {
+ log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.formatInfo().fourcc()));
}
if(log.isDebugEnabled()) {
- log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
+ log.debug("Opening capture stream on " + device + " with format " + format.formatInfo().width() + "x" + format.formatInfo().height() + " (" + WebcamPixelFormat.fourCCToString(format.formatInfo().fourcc()) + ")");
}
opening.set(true);
@@ -203,7 +211,7 @@ public class WebcamService extends ScheduledService<Image> {
opening.set(false);
try {
- zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
+ zoomLimits = stream.getPropertyLimits(CaptureProperty.ZOOM);
} catch(Throwable e) {
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
}
@@ -285,7 +293,7 @@ public class WebcamService extends ScheduledService<Image> {
public int getZoom() {
if(stream != null && zoomLimits != null) {
try {
- return stream.getProperty(CaptureProperty.Zoom);
+ return stream.getProperty(CaptureProperty.ZOOM);
} catch(Exception e) {
log.error("Error getting zoom property on " + device, e);
}
@@ -297,7 +305,7 @@ public class WebcamService extends ScheduledService<Image> {
public void setZoom(int value) {
if(stream != null && zoomLimits != null) {
try {
- stream.setProperty(CaptureProperty.Zoom, value);
+ stream.setProperty(CaptureProperty.ZOOM, value);
} catch(Exception e) {
log.error("Error setting zoom property on " + device, e);
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java
index 5e9b87f..796d50c 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java
@@ -51,8 +51,8 @@ public class WebcamView {
int currentZoom = service.getZoom();
if(currentZoom >= 0) {
int newZoom = scrollEvent.getDeltaY() > 0 ? Math.round(currentZoom * 1.1f) : Math.round(currentZoom * 0.9f);
- newZoom = Math.max(newZoom, service.getZoomLimits().getMin());
- newZoom = Math.min(newZoom, service.getZoomLimits().getMax());
+ newZoom = Math.max(newZoom, service.getZoomLimits().min());
+ newZoom = Math.min(newZoom, service.getZoomLimits().max());
if(newZoom != currentZoom) {
service.setZoom(newZoom);
}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 0c92d2e..d7020b9 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -33,7 +33,6 @@ open module com.sparrowwallet.sparrow {
requires com.h2database;
requires com.sparrowwallet.hummingbird;
requires org.fxmisc.flowless;
- requires openpnp.capture.java;
requires nsmenufx;
requires org.jcommander;
requires jul.to.slf4j;
@@ -55,6 +54,6 @@ open module com.sparrowwallet.sparrow {
requires com.jcraft.jzlib;
requires com.sparrowwallet.tern;
requires com.sparrowwallet.lark;
- requires com.sun.jna;
requires io.github.doblon8.jzbar;
+ requires io.github.doblon8.openpnp.capture;
}
\ No newline at end of file
Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.