Skip to content

Commit f36f61b

Browse files
committed
Extract NativeLibraryPermissionSetter from LlamaLoader
Replace the static setNativeLibraryPermissions helper with a small NativeLibraryPermissionSetter class instantiated once per LlamaLoader. The PrintStream warning sink is injected via constructor; apply(File) is an instance method, so no static logic remains. Tests move to a dedicated NativeLibraryPermissionSetterTest covering all-success, each-setter-fails, all-fail, and null-sink rejection paths.
1 parent 99b4409 commit f36f61b

4 files changed

Lines changed: 162 additions & 117 deletions

File tree

src/main/java/net/ladenthin/llama/LlamaLoader.java

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.io.File;
1010
import java.io.IOException;
1111
import java.io.InputStream;
12-
import java.io.PrintStream;
1312
import java.nio.file.Files;
1413
import java.nio.file.Path;
1514
import java.nio.file.Paths;
@@ -36,6 +35,8 @@ class LlamaLoader {
3635

3736
private static boolean extracted = false;
3837
private static final LlamaSystemProperties systemProperties = new LlamaSystemProperties();
38+
private static final NativeLibraryPermissionSetter permissionSetter =
39+
new NativeLibraryPermissionSetter(System.err);
3940

4041
/**
4142
* Loads the llama and jllama shared libraries
@@ -192,7 +193,7 @@ private static Path extractFile(String sourceDirectory, String fileName, String
192193
}
193194

194195
// Set executable (x) flag to enable Java to load the native library
195-
setNativeLibraryPermissions(extractedFilePath.toFile(), System.err);
196+
permissionSetter.apply(extractedFilePath.toFile());
196197

197198
// Check whether the contents are properly copied from the resource folder
198199
try (InputStream nativeIn = LlamaLoader.class.getResourceAsStream(nativeLibraryFilePath);
@@ -211,32 +212,6 @@ private static Path extractFile(String sourceDirectory, String fileName, String
211212
}
212213
}
213214

214-
/**
215-
* Sets read, write (owner-only), and execute permissions on the given file so the JVM
216-
* can load it as a native library. Returns {@code false} and writes a warning to
217-
* {@code err} if any of the permission changes were rejected by the platform.
218-
*
219-
* <p>Package-private and parameterised on {@code File}/{@code PrintStream} to keep the
220-
* branch unit-testable.
221-
*
222-
* @param file the extracted native library file
223-
* @param err stream to receive a warning when a permission change fails
224-
* @return {@code true} if all three permission changes succeeded
225-
*/
226-
static boolean setNativeLibraryPermissions(File file, PrintStream err) {
227-
boolean readable = file.setReadable(true);
228-
boolean writable = file.setWritable(true, true);
229-
boolean executable = file.setExecutable(true);
230-
if (!readable || !writable || !executable) {
231-
err.println("Warning: could not set permissions on " + file
232-
+ " (readable=" + readable
233-
+ ", writable=" + writable
234-
+ ", executable=" + executable + ")");
235-
return false;
236-
}
237-
return true;
238-
}
239-
240215
/**
241216
* Extracts and loads the specified library file to the target folder
242217
*
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
package net.ladenthin.llama;
6+
7+
import java.io.File;
8+
import java.io.PrintStream;
9+
import java.util.Objects;
10+
11+
/**
12+
* Applies the read / write (owner-only) / execute permissions required for the
13+
* JVM to load an extracted native library file.
14+
*
15+
* <p>The three {@link File} setter calls each return a {@code boolean}; this
16+
* class observes those return values and writes a descriptive warning to a
17+
* configurable {@link PrintStream} when any permission change is rejected by
18+
* the platform. Both the warning sink and the entry point are instance members
19+
* so the behaviour can be unit-tested without touching {@link System#err}.
20+
*/
21+
final class NativeLibraryPermissionSetter {
22+
23+
private final PrintStream warningSink;
24+
25+
NativeLibraryPermissionSetter(PrintStream warningSink) {
26+
this.warningSink = Objects.requireNonNull(warningSink, "warningSink");
27+
}
28+
29+
/**
30+
* Sets read, owner-only write, and execute permissions on {@code file}.
31+
*
32+
* @param file the extracted native library file
33+
* @return {@code true} if all three permission changes succeeded
34+
*/
35+
boolean apply(File file) {
36+
boolean readable = file.setReadable(true);
37+
boolean writable = file.setWritable(true, true);
38+
boolean executable = file.setExecutable(true);
39+
if (!readable || !writable || !executable) {
40+
warningSink.println("Warning: could not set permissions on " + file
41+
+ " (readable=" + readable
42+
+ ", writable=" + writable
43+
+ ", executable=" + executable + ")");
44+
return false;
45+
}
46+
return true;
47+
}
48+
}

src/test/java/net/ladenthin/llama/LlamaLoaderTest.java

Lines changed: 2 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77

88
import java.io.ByteArrayInputStream;
99
import java.io.BufferedInputStream;
10-
import java.io.ByteArrayOutputStream;
1110
import java.io.File;
1211
import java.io.IOException;
1312
import java.io.InputStream;
14-
import java.io.PrintStream;
1513
import java.nio.file.Paths;
1614

1715
import org.junit.After;
@@ -25,10 +23,8 @@
2523
"native library: shouldCleanPath detects jllama/llama-prefixed files for " +
2624
"cleanup; contentsEquals performs a correct byte-level stream comparison " +
2725
"including BufferedInputStream wrapping and length mismatches; getTempDir " +
28-
"honours the 'net.ladenthin.llama.tmpdir' system-property override; " +
29-
"getNativeResourcePath produces the expected classpath resource prefix; and " +
30-
"setNativeLibraryPermissions returns the AND of its three File setter calls " +
31-
"and emits a descriptive warning to the given PrintStream when any fails."
26+
"honours the 'net.ladenthin.llama.tmpdir' system-property override; and " +
27+
"getNativeResourcePath produces the expected classpath resource prefix."
3228
)
3329
public class LlamaLoaderTest {
3430

@@ -210,87 +206,4 @@ public void testGetNativeResourcePathContainsOsAndArch() {
210206
assertTrue("Resource path should end with OS/arch: " + path,
211207
path.endsWith(osArch));
212208
}
213-
214-
// -------------------------------------------------------------------------
215-
// setNativeLibraryPermissions
216-
// -------------------------------------------------------------------------
217-
218-
/** Stub File whose setReadable/setWritable/setExecutable returns are configurable. */
219-
private static class StubFile extends File {
220-
final boolean readable;
221-
final boolean writable;
222-
final boolean executable;
223-
224-
StubFile(boolean readable, boolean writable, boolean executable) {
225-
super("stub-native-lib");
226-
this.readable = readable;
227-
this.writable = writable;
228-
this.executable = executable;
229-
}
230-
231-
@Override
232-
public boolean setReadable(boolean r) { return readable; }
233-
234-
@Override
235-
public boolean setWritable(boolean w, boolean ownerOnly) { return writable; }
236-
237-
@Override
238-
public boolean setExecutable(boolean x) { return executable; }
239-
}
240-
241-
private static PrintStream capture(ByteArrayOutputStream sink) {
242-
return new PrintStream(sink);
243-
}
244-
245-
@Test
246-
public void testSetNativeLibraryPermissionsAllSucceed() {
247-
ByteArrayOutputStream sink = new ByteArrayOutputStream();
248-
boolean ok = LlamaLoader.setNativeLibraryPermissions(
249-
new StubFile(true, true, true), capture(sink));
250-
assertTrue("expected success when all setters return true", ok);
251-
assertEquals("no warning expected on success", "", sink.toString());
252-
}
253-
254-
@Test
255-
public void testSetNativeLibraryPermissionsReadableFails() {
256-
ByteArrayOutputStream sink = new ByteArrayOutputStream();
257-
boolean ok = LlamaLoader.setNativeLibraryPermissions(
258-
new StubFile(false, true, true), capture(sink));
259-
assertFalse(ok);
260-
String out = sink.toString();
261-
assertTrue("warning should mention readable=false: " + out, out.contains("readable=false"));
262-
assertTrue("warning should mention writable=true: " + out, out.contains("writable=true"));
263-
assertTrue("warning should mention executable=true: " + out, out.contains("executable=true"));
264-
assertTrue("warning should mention file path: " + out, out.contains("stub-native-lib"));
265-
}
266-
267-
@Test
268-
public void testSetNativeLibraryPermissionsWritableFails() {
269-
ByteArrayOutputStream sink = new ByteArrayOutputStream();
270-
boolean ok = LlamaLoader.setNativeLibraryPermissions(
271-
new StubFile(true, false, true), capture(sink));
272-
assertFalse(ok);
273-
assertTrue(sink.toString().contains("writable=false"));
274-
}
275-
276-
@Test
277-
public void testSetNativeLibraryPermissionsExecutableFails() {
278-
ByteArrayOutputStream sink = new ByteArrayOutputStream();
279-
boolean ok = LlamaLoader.setNativeLibraryPermissions(
280-
new StubFile(true, true, false), capture(sink));
281-
assertFalse(ok);
282-
assertTrue(sink.toString().contains("executable=false"));
283-
}
284-
285-
@Test
286-
public void testSetNativeLibraryPermissionsAllFail() {
287-
ByteArrayOutputStream sink = new ByteArrayOutputStream();
288-
boolean ok = LlamaLoader.setNativeLibraryPermissions(
289-
new StubFile(false, false, false), capture(sink));
290-
assertFalse(ok);
291-
String out = sink.toString();
292-
assertTrue(out.contains("readable=false"));
293-
assertTrue(out.contains("writable=false"));
294-
assertTrue(out.contains("executable=false"));
295-
}
296209
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
package net.ladenthin.llama;
6+
7+
import java.io.ByteArrayOutputStream;
8+
import java.io.File;
9+
import java.io.PrintStream;
10+
11+
import org.junit.Test;
12+
13+
import static org.junit.Assert.assertEquals;
14+
import static org.junit.Assert.assertFalse;
15+
import static org.junit.Assert.assertTrue;
16+
17+
@ClaudeGenerated(
18+
purpose = "Verify NativeLibraryPermissionSetter.apply(File): returns the AND of " +
19+
"the three File setter calls (setReadable, setWritable, setExecutable) and " +
20+
"emits a descriptive warning to the injected PrintStream when any setter " +
21+
"returns false. Uses a StubFile subclass so the test does not touch disk."
22+
)
23+
public class NativeLibraryPermissionSetterTest {
24+
25+
/** Stub File whose setReadable/setWritable/setExecutable returns are configurable. */
26+
private static class StubFile extends File {
27+
final boolean readable;
28+
final boolean writable;
29+
final boolean executable;
30+
31+
StubFile(boolean readable, boolean writable, boolean executable) {
32+
super("stub-native-lib");
33+
this.readable = readable;
34+
this.writable = writable;
35+
this.executable = executable;
36+
}
37+
38+
@Override
39+
public boolean setReadable(boolean r) {
40+
return readable;
41+
}
42+
43+
@Override
44+
public boolean setWritable(boolean w, boolean ownerOnly) {
45+
return writable;
46+
}
47+
48+
@Override
49+
public boolean setExecutable(boolean x) {
50+
return executable;
51+
}
52+
}
53+
54+
private ByteArrayOutputStream sink;
55+
private NativeLibraryPermissionSetter setter;
56+
57+
private void setUp() {
58+
sink = new ByteArrayOutputStream();
59+
setter = new NativeLibraryPermissionSetter(new PrintStream(sink));
60+
}
61+
62+
@Test
63+
public void testApplyAllSucceed() {
64+
setUp();
65+
assertTrue("expected success when all setters return true",
66+
setter.apply(new StubFile(true, true, true)));
67+
assertEquals("no warning expected on success", "", sink.toString());
68+
}
69+
70+
@Test
71+
public void testApplyReadableFails() {
72+
setUp();
73+
assertFalse(setter.apply(new StubFile(false, true, true)));
74+
String out = sink.toString();
75+
assertTrue("warning should mention readable=false: " + out, out.contains("readable=false"));
76+
assertTrue("warning should mention writable=true: " + out, out.contains("writable=true"));
77+
assertTrue("warning should mention executable=true: " + out, out.contains("executable=true"));
78+
assertTrue("warning should mention file path: " + out, out.contains("stub-native-lib"));
79+
}
80+
81+
@Test
82+
public void testApplyWritableFails() {
83+
setUp();
84+
assertFalse(setter.apply(new StubFile(true, false, true)));
85+
assertTrue(sink.toString().contains("writable=false"));
86+
}
87+
88+
@Test
89+
public void testApplyExecutableFails() {
90+
setUp();
91+
assertFalse(setter.apply(new StubFile(true, true, false)));
92+
assertTrue(sink.toString().contains("executable=false"));
93+
}
94+
95+
@Test
96+
public void testApplyAllFail() {
97+
setUp();
98+
assertFalse(setter.apply(new StubFile(false, false, false)));
99+
String out = sink.toString();
100+
assertTrue(out.contains("readable=false"));
101+
assertTrue(out.contains("writable=false"));
102+
assertTrue(out.contains("executable=false"));
103+
}
104+
105+
@Test(expected = NullPointerException.class)
106+
public void testConstructorRejectsNullSink() {
107+
new NativeLibraryPermissionSetter(null);
108+
}
109+
}

0 commit comments

Comments
 (0)