added PathClassLoader
This commit is contained in:
36
build.gradle
36
build.gradle
@@ -6,8 +6,9 @@ plugins {
|
|||||||
allprojects {
|
allprojects {
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
apply plugin: 'net.woggioni.gradle.lombok'
|
apply plugin: 'net.woggioni.gradle.lombok'
|
||||||
|
|
||||||
group = "net.woggioni"
|
group = "net.woggioni"
|
||||||
version = jwoVersion
|
version = getProperty('jwo.version')
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
@@ -16,17 +17,17 @@ allprojects {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', 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: junitJupiterVersion
|
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: getProperty('junitJupiter.version')
|
||||||
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitJupiterVersion
|
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: getProperty('junitJupiter.version')
|
||||||
}
|
}
|
||||||
lombok {
|
lombok {
|
||||||
version = lombokVersion
|
version = getProperty('lombok.version')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation group: "org.slf4j", name: "slf4j-api", version: slf4jVersion
|
implementation group: "org.slf4j", name: "slf4j-api", version: getProperty('slf4j.version')
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava {
|
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 {
|
wrapper {
|
||||||
distributionType = Wrapper.DistributionType.BIN
|
distributionType = Wrapper.DistributionType.BIN
|
||||||
gradleVersion = "7.0.2"
|
gradleVersion = getProperty('gradle.version')
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
jwoVersion=1.0
|
gradle.version = 7.1
|
||||||
junitJupiterVersion=5.7.0
|
jwo.version=1.0
|
||||||
lombokVersion=1.18.16
|
junitJupiter.version=5.7.0
|
||||||
slf4jVersion=1.7.30
|
lombok.version=1.18.16
|
||||||
|
slf4j.version=1.7.30
|
||||||
|
86
src/main/java/net/woggioni/jwo/hash/Hash.java
Normal file
86
src/main/java/net/woggioni/jwo/hash/Hash.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
@@ -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() { }
|
||||||
|
}
|
||||||
|
|
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
93
src/main/java/net/woggioni/jwo/loader/PathClassLoader.java
Normal file
93
src/main/java/net/woggioni/jwo/loader/PathClassLoader.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
57
src/main/java/net/woggioni/jwo/loader/PathURLConnection.java
Normal file
57
src/main/java/net/woggioni/jwo/loader/PathURLConnection.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
138
src/test/java/net/woggioni/jwo/io/ExtractorInputStreamTest.java
Normal file
138
src/test/java/net/woggioni/jwo/io/ExtractorInputStreamTest.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user