added PathClassLoader
This commit is contained in:
36
build.gradle
36
build.gradle
@@ -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 {
|
||||
|
@@ -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
|
||||
|
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