added PathClassLoader

This commit is contained in:
2021-07-01 21:36:38 +01:00
parent 20b5df20d0
commit 299367fb74
13 changed files with 686 additions and 50 deletions

View File

@@ -6,8 +6,9 @@ plugins {
allprojects {
apply plugin: 'java-library'
apply plugin: 'net.woggioni.gradle.lombok'
group = "net.woggioni"
version = jwoVersion
version = getProperty('jwo.version')
repositories {
maven {
@@ -16,17 +17,17 @@ allprojects {
mavenCentral()
}
dependencies {
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junitJupiterVersion
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junitJupiterVersion
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitJupiterVersion
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: getProperty('junitJupiter.version')
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: getProperty('junitJupiter.version')
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: getProperty('junitJupiter.version')
}
lombok {
version = lombokVersion
version = getProperty('lombok.version')
}
}
dependencies {
implementation group: "org.slf4j", name: "slf4j-api", version: slf4jVersion
implementation group: "org.slf4j", name: "slf4j-api", version: getProperty('slf4j.version')
}
compileJava {
@@ -41,9 +42,30 @@ jar {
}
}
test {
useJUnitPlatform()
Dependency junitJupiterEngineDependency =
dependencies.create(
group: 'org.junit.jupiter',
name: 'junit-jupiter-engine',
version: project.getProperty('junitJupiter.version')
)
File junitJupiterEngineJar = configurations.getByName(JavaPlugin.TEST_RUNTIME_CLASSPATH_CONFIGURATION_NAME)
.resolvedConfiguration.resolvedArtifacts.grep { ResolvedArtifact resolvedArtifact ->
ModuleVersionIdentifier id = resolvedArtifact.moduleVersion.id
id.group == 'org.junit.jupiter' && id.name == 'junit-jupiter-engine'
}.collect {
ResolvedArtifact resolvedArtifact -> resolvedArtifact.file
}.first()
systemProperties([
'junit.jupiter.engine.jar' : junitJupiterEngineJar.toString()
])
}
wrapper {
distributionType = Wrapper.DistributionType.BIN
gradleVersion = "7.0.2"
gradleVersion = getProperty('gradle.version')
}
publishing {

View File

@@ -1,4 +1,5 @@
jwoVersion=1.0
junitJupiterVersion=5.7.0
lombokVersion=1.18.16
slf4jVersion=1.7.30
gradle.version = 7.1
jwo.version=1.0
junitJupiter.version=5.7.0
lombok.version=1.18.16
slf4j.version=1.7.30

View File

@@ -0,0 +1,86 @@
package net.woggioni.jwo.hash;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.Arrays;
@RequiredArgsConstructor
public class Hash {
@RequiredArgsConstructor
enum Algorithm {
MD2("MD2"),
MD5("MD5"),
SHA1("SHA-1"),
SHA256("SHA-256"),
SHA384("SHA-384"),
SHA512("SHA-512");
private final String key;
}
final Algorithm algorithm;
final byte[] bytes;
@Override
public boolean equals(Object other) {
if(other == null) return false;
else if(getClass() != other.getClass()) return false;
Hash otherHash = (Hash) other;
if(algorithm != otherHash.algorithm) return false;
return Arrays.equals(bytes, otherHash.bytes);
}
@Override
public int hashCode() {
int result = algorithm.hashCode();
for(byte b : bytes) {
result ^= b;
}
return result;
}
@SneakyThrows
public static Hash hash(Algorithm algo, InputStream is, byte[] buffer) {
MessageDigest md = MessageDigest.getInstance(algo.key);
int read;
while((read = is.read(buffer, 0, buffer.length)) >= 0) {
md.update(buffer, 0, read);
}
return new Hash(algo, md.digest());
}
@SneakyThrows
public static Hash hash(Algorithm algo, InputStream is) {
return hash(algo, is, new byte[0x1000]);
}
@SneakyThrows
public static Hash md5(InputStream is) {
return md5(is, new byte[0x1000]);
}
@SneakyThrows
public static Hash md5(InputStream is, byte[] buffer) {
return hash(Algorithm.MD5, is, buffer);
}
public static String md5String(InputStream is) {
return bytesToHex(md5(is).bytes);
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for(int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}

View File

@@ -1,39 +0,0 @@
package net.woggioni.jwo.hash;
import lombok.SneakyThrows;
import java.io.InputStream;
import java.security.MessageDigest;
public class Hasher {
private Hasher() {}
@SneakyThrows
public static byte[] md5(InputStream is) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[1024];
int read;
while((read = is.read(buffer, 0, buffer.length)) >= 0) {
md.update(buffer, 0, read);
}
return md.digest();
}
public static String md5String(InputStream is) {
return bytesToHex(md5(is));
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for(int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}

View File

@@ -0,0 +1,83 @@
package net.woggioni.jwo.io;
import net.woggioni.jwo.JWO;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* Input stream that extract a jar archive in the provided {@param destination} while reading it
*/
class JarExtractorInputStream extends JarInputStream {
private final String sourceLocation;
private final Path destination;
private OutputStream currentFile = null;
public JarExtractorInputStream(InputStream source,
Path destination,
boolean verify,
String sourceLocation) throws IOException {
super(source, verify);
this.sourceLocation = sourceLocation;
this.destination = destination;
Path newFileSystemLocation = destination.resolve(JarFile.MANIFEST_NAME);
Files.createDirectories(newFileSystemLocation.getParent());
try(OutputStream outputStream = Files.newOutputStream(newFileSystemLocation)) {
Manifest manifest = getManifest();
if(manifest == null) {
String location;
if(sourceLocation == null) {
location = "";
} else {
location = String.format("from '%s'", sourceLocation);
}
throw JWO.newThrowable(IOException.class,
"The source stream %s doesn't represent a valid jar file", location);
}
manifest.write(outputStream);
}
}
@Override
public ZipEntry getNextEntry() throws IOException {
ZipEntry entry = super.getNextEntry();
if(entry != null) {
Path newFileSystemLocation = destination.resolve(entry.getName());
if(entry.isDirectory()) {
Files.createDirectories(newFileSystemLocation);
} else {
Files.createDirectories(newFileSystemLocation.getParent());
currentFile = Files.newOutputStream(newFileSystemLocation);
}
}
return entry;
}
@Override
public int read() throws IOException {
int result = super.read();
if(result != -1 && currentFile != null) currentFile.write(result);
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
if(read != -1 && currentFile != null) currentFile.write(b, off, read);
return read;
}
@Override
public void closeEntry() throws IOException{
super.closeEntry();
if(currentFile != null) currentFile.close();
}
}

View File

@@ -0,0 +1,20 @@
package net.woggioni.jwo.io;
import java.io.FilterInputStream;
import java.io.InputStream;
/**
* {@link InputStream} wrapper that prevents it from being closed, useful to pass an {@link InputStream} instance
* to a method that closes the stream before it has been fully consumed
* (and whose remaining content is still needed by the caller)
*/
public class UncloseableInputStream extends FilterInputStream {
public UncloseableInputStream(InputStream source) {
super(source);
}
@Override
public void close() { }
}

View File

@@ -0,0 +1,19 @@
package net.woggioni.jwo.io;
import java.io.FilterOutputStream;
import java.io.OutputStream;
/**
* {@link OutputStream} wrapper that prevents it from being closed, useful to pass an {@link OutputStream} instance
* to a method that closes the stream before it has been finalized by the caller
*/
public class UncloseableOutputStream extends FilterOutputStream {
public UncloseableOutputStream(OutputStream source) {
super(source);
}
@Override
public void close() {
}
}

View File

@@ -0,0 +1,59 @@
package net.woggioni.jwo.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Input stream that extract a zip archive in the provided {@param destination} while reading it
*/
class ZipExtractorInputStream extends ZipInputStream {
public ZipExtractorInputStream(InputStream source, Path destination) {
super(source);
this.destination = destination;
}
private final Path destination;
private OutputStream currentFile = null;
@Override
public ZipEntry getNextEntry() throws IOException {
ZipEntry entry = super.getNextEntry();
if(entry != null) {
Path newFileSystemLocation = destination.resolve(entry.getName());
if(entry.isDirectory()) {
Files.createDirectories(newFileSystemLocation);
} else {
Files.createDirectories(newFileSystemLocation.getParent());
currentFile = Files.newOutputStream(newFileSystemLocation);
}
}
return entry;
}
@Override
public int read() throws IOException {
int result = super.read();
if(result != -1 && currentFile != null) currentFile.write(result);
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
if(read != -1 && currentFile != null) currentFile.write(b, off, read);
return read;
}
@Override
public void closeEntry() throws IOException{
super.closeEntry();
if(currentFile != null) currentFile.close();
}
}

View File

@@ -0,0 +1,93 @@
package net.woggioni.jwo.loader;
import lombok.SneakyThrows;
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* A classloader that loads classes from a {@link Path} instance
*/
public final class PathClassLoader extends ClassLoader {
private final Path path;
static {
registerAsParallelCapable();
}
public PathClassLoader(Path path) {
this(path, null);
}
public PathClassLoader(Path path, ClassLoader parent) {
super(parent);
this.path = path;
}
@Override
@SneakyThrows
protected Class<?> findClass(String name) {
Path classPath = path.resolve(name.replace('.', '/').concat(".class"));
if (Files.exists(classPath)) {
byte[] byteCode = Files.readAllBytes(classPath);
return defineClass(name, byteCode, 0, byteCode.length);
} else {
throw new ClassNotFoundException(name);
}
}
@Override
@SneakyThrows
protected URL findResource(String name) {
Path resolved = path.resolve(name);
if (Files.exists(resolved)) {
return toURL(resolved);
} else {
return null;
}
}
@Override
protected Enumeration<URL> findResources(final String name) throws IOException {
final List<URL> resources = new ArrayList<>(1);
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (!name.isEmpty()) {
this.addIfMatches(resources, file);
}
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (!name.isEmpty() || path.equals(dir)) {
this.addIfMatches(resources, dir);
}
return super.preVisitDirectory(dir, attrs);
}
void addIfMatches(List<URL> resources, Path file) throws IOException {
if (path.relativize(file).toString().equals(name)) {
resources.add(toURL(file));
}
}
});
return Collections.enumeration(resources);
}
private static URL toURL(Path path) throws IOException {
return new URL(null, path.toUri().toString(), PathURLStreamHandler.INSTANCE);
}
}

View File

@@ -0,0 +1,57 @@
package net.woggioni.jwo.loader;
import lombok.SneakyThrows;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
final class PathURLConnection extends URLConnection {
private final Path path;
PathURLConnection(URL url, Path path) {
super(url);
this.path = path;
}
@Override
public void connect() {}
@Override
public long getContentLengthLong() {
try {
return Files.size(this.path);
} catch (IOException e) {
throw new RuntimeException("could not get size of: " + this.path, e);
}
}
@Override
public InputStream getInputStream() throws IOException {
return Files.newInputStream(this.path);
}
@Override
public OutputStream getOutputStream() throws IOException {
return Files.newOutputStream(this.path);
}
@Override
@SneakyThrows
public String getContentType() {
return Files.probeContentType(this.path);
}
@Override
@SneakyThrows
public long getLastModified() {
BasicFileAttributes attributes = Files.readAttributes(this.path, BasicFileAttributes.class);
return attributes.lastModifiedTime().toMillis();
}
}

View File

@@ -0,0 +1,26 @@
package net.woggioni.jwo.loader;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class PathURLStreamHandler extends URLStreamHandler {
static final URLStreamHandler INSTANCE = new PathURLStreamHandler();
@Override
@SneakyThrows
protected URLConnection openConnection(URL url) {
URI uri = url.toURI();
Path path = Paths.get(uri);
return new PathURLConnection(url, path);
}
}

View File

@@ -0,0 +1,71 @@
package net.woggioni.jwo.signing;
import java.security.CodeSigner;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.regex.Pattern;
/**
* Helper class to extract signatures from a jar file, it has to be used calling {@link #addEntry} on all of the jar's {@link JarEntry}
* after having consumed their entry content from the source (@link java.util.jar.JarInputStream}, then {@link #getCertificates()}
* will return the public keys of the jar's signers.
*/
class SignatureCollector {
/**
* @see <https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File>
* Additionally accepting *.EC as its valid for [java.util.jar.JarVerifier] and jarsigner @see https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jarsigner.html,
* temporally treating META-INF/INDEX.LIST as unsignable entry because [java.util.jar.JarVerifier] doesn't load its signers.
*/
private static final Pattern unsignableEntryName = Pattern.compile("META-INF/(?:(?:.*[.](?:SF|DSA|RSA|EC)|SIG-.*)|INDEX\\.LIST)");
/**
* @return if the [entry] [JarEntry] can be signed.
*/
static boolean isSignable(JarEntry entry) {
return !entry.isDirectory() && !unsignableEntryName.matcher(entry.getName()).matches();
}
private static Set<PublicKey> signers2OrderedPublicKeys(CodeSigner[] signers) {
Set<PublicKey> result = new LinkedHashSet<>();
for (CodeSigner signer : signers) {
result.add((signer.getSignerCertPath().getCertificates().get(0)).getPublicKey());
}
return Collections.unmodifiableSet(result);
}
private String firstSignedEntry = null;
private CodeSigner[] codeSigners = null;
private Set<Certificate> _certificates;
public final Set<Certificate> getCertificates() {
return Collections.unmodifiableSet(_certificates);
}
public void addEntry(JarEntry jarEntry) {
if (isSignable(jarEntry)) {
CodeSigner[] entrySigners = jarEntry.getCodeSigners() != null ? jarEntry.getCodeSigners() : new CodeSigner[0];
if (codeSigners == null) {
codeSigners = entrySigners;
firstSignedEntry = jarEntry.getName();
for (CodeSigner signer : entrySigners) {
_certificates.add(signer.getSignerCertPath().getCertificates().get(0));
}
}
if (!Arrays.equals(codeSigners, entrySigners)) {
throw new IllegalArgumentException(String.format(
"Mismatch between signers %s for file %s and signers %s for file %s",
signers2OrderedPublicKeys(codeSigners),
firstSignedEntry,
signers2OrderedPublicKeys(entrySigners),
jarEntry.getName()));
}
}
}
}

View File

@@ -0,0 +1,138 @@
package net.woggioni.jwo.io;
import lombok.SneakyThrows;
import net.woggioni.jwo.JWO;
import net.woggioni.jwo.hash.Hash;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ExtractorInputStreamTest {
private Path testDir;
private Path testJar;
private Path referenceExtractionDestination;
private Path testExtractionDestination;
@BeforeEach
void setup(@TempDir Path testDir) {
this.testDir = testDir;
testJar = Path.of(System.getProperty("junit.jupiter.engine.jar"));
referenceExtractionDestination = testDir.resolve("referenceExtraction");
testExtractionDestination = testDir.resolve("testExtraction");
}
@SneakyThrows
private static void referenceUnzipMethod(Path source, Path destination) {
try(FileSystem fs = FileSystems.newFileSystem(source, null)) {
for(Path root : fs.getRootDirectories()) {
Files.walk(root)
.filter(Predicate.not(Files::isDirectory)).forEach(new Consumer<Path>() {
@Override
@SneakyThrows
public void accept(Path path) {
Path newDir = destination.resolve(root.relativize(path).toString());
Files.createDirectories(newDir.getParent());
Files.copy(path, newDir);
}
});
}
}
}
@SneakyThrows
private static NavigableMap<String, Hash> hashFileTree(Path tree) {
NavigableMap<String, Hash> result = new TreeMap<>();
byte[] buffer = new byte[0x1000];
FileVisitor<Path> visitor = new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String key = tree.relativize(file).toString();
if(!Objects.equals(JarFile.MANIFEST_NAME, key)) {
try (InputStream is = Files.newInputStream(file)) {
result.put(key, Hash.md5(is, buffer));
}
} else {
Manifest manifest = new Manifest();
try (InputStream is = Files.newInputStream(file)) {
manifest.read(is);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
manifest.write(baos);
} finally {
baos.close();
}
result.put(key, Hash.md5(new ByteArrayInputStream(baos.toByteArray()), buffer));
}
return FileVisitResult.CONTINUE;
}
};
Files.walkFileTree(tree, visitor);
return result;
}
private static boolean compareFileTree(Path tree1, Path tree2) {
NavigableMap<String, Hash> hash1 = hashFileTree(tree1);
NavigableMap<String, Hash> hash2 = hashFileTree(tree2);
return Objects.equals(hash1, hash2);
}
@SneakyThrows
public void run(Supplier<ZipInputStream> zipInputStreamSupplier) {
referenceUnzipMethod(testJar, referenceExtractionDestination);
try(ZipInputStream zipInputStream = zipInputStreamSupplier.get()) {
while(true) {
ZipEntry zipEntry = zipInputStream.getNextEntry();
if(zipEntry == null) break;
JWO.write2Stream(new NullOutputStream(), zipInputStream);
zipInputStream.closeEntry();
}
}
Assertions.assertTrue(compareFileTree(referenceExtractionDestination, testExtractionDestination));
}
@Test
@SneakyThrows
public void zipExtractorInputStreamTest() {
Supplier<ZipInputStream> supplier = new Supplier<ZipInputStream>() {
@Override
@SneakyThrows
public ZipInputStream get() {
return new ZipExtractorInputStream(Files.newInputStream(testJar), testExtractionDestination);
}
};
run(supplier);
}
@Test
@SneakyThrows
public void jarExtractorInputStreamTest() {
Supplier<ZipInputStream> supplier = new Supplier<ZipInputStream>() {
@Override
@SneakyThrows
public ZipInputStream get() {
return new JarExtractorInputStream(Files.newInputStream(testJar), testExtractionDestination, true, null);
}
};
run(supplier);
}
}