Skip to content

Commit

Permalink
Hacking
Browse files Browse the repository at this point in the history
  • Loading branch information
snicoll committed Nov 8, 2023
1 parent 690cfa2 commit 9bf4b2e
Show file tree
Hide file tree
Showing 7 changed files with 747 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.jarmode.layertools;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes.Name;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.springframework.boot.jarmode.layertools.JarStructure.Entry;
import org.springframework.util.Assert;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;

/**
* Hacking.
*
* @author Stephane Nicoll
*/
class ExtractJarCommand extends Command {

static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to");

private final Context context;

private final JarStructure jarStructure;

ExtractJarCommand(Context context) {
this(context, IndexedJarStructure.get(context));
}

ExtractJarCommand(Context context, JarStructure jarStructure) {
super("extract2", "Extracts the application in optimized structure", Options.of(DESTINATION_OPTION),
Parameters.none());
this.context = context;
this.jarStructure = jarStructure;
}

@Override
protected void run(Map<Option, String> options, List<String> parameters) {
try {
Path destination = options.containsKey(DESTINATION_OPTION) ? Paths.get(options.get(DESTINATION_OPTION))
: this.context.getWorkingDir().toPath();
Path application = destination.resolve("application");
mkDirs(application);
Path dependencies = destination.resolve("dependencies");
mkDirs(dependencies);
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) {
ZipEntry zipEntry = zip.getNextEntry();
Assert.state(zipEntry != null, "File '" + this.context.getArchiveFile().toString()
+ "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled");
while (zipEntry != null) {
Entry resolvedEntry = this.jarStructure.resolve(zipEntry);
if (resolvedEntry != null) {
String location = (resolvedEntry.library() ? "dependencies/" + resolvedEntry.location()
: "application/" + resolvedEntry.location());
write(zip, zipEntry, destination, location);
}
zipEntry = zip.getNextEntry();
}
}
// Create the application jar
Path applicationJarFile = createApplicationJar(destination, application);

// create the run-app.jar
createRunAppJar(destination, applicationJarFile);

}
catch (IOException ex) {
throw new IllegalStateException(ex);
}

}

private Path createApplicationJar(Path destination, Path application) throws IOException {
String archiveName = this.context.getArchiveFile().getName();
Path archive = createArchiveFromDirectory(destination, application, archiveName);
FileSystemUtils.deleteRecursively(application);
mkDirs(application);
return Files.move(archive, application.resolve(archive.getFileName()));
}

// Hacking
private Path createArchiveFromDirectory(Path destination, Path applicationDirectory, String jarName)
throws IOException {
Process process = new ProcessBuilder().inheritIO()
.directory(destination.toFile())
.command("jar", "cf", jarName, "-C", applicationDirectory.getFileName().toString(), ".")
.start();
try {
int exit = process.waitFor();
if (exit != 0) {
throw new IllegalStateException("Jar file creation failed");
}
}
catch (InterruptedException ex) {
throw new IllegalStateException(ex);
}
return destination.resolve(jarName);
}

private void createRunAppJar(Path destination, Path applicationJarFile) throws IOException {
Manifest manifest = this.jarStructure.createRunJarManifest((dependency) -> "dependencies/" + dependency);
String libs = manifest.getMainAttributes().getValue(Name.CLASS_PATH);
String applicationJar = destination.relativize(applicationJarFile).toString();
manifest.getMainAttributes().put(Name.CLASS_PATH, String.join(" ", applicationJar, libs));
Path runJar = destination.resolve("run-app.jar");
try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(runJar), manifest)) {
// EMPTY
}
}

private void write(ZipInputStream zip, ZipEntry entry, Path destination, String name) throws IOException {
String canonicalOutputPath = destination.toFile().getCanonicalPath() + File.separator;
Path file = destination.resolve(name);
String canonicalEntryPath = file.toFile().getCanonicalPath();
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
() -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath
+ "'. This is outside the output location of '" + canonicalOutputPath
+ "'. Verify the contents of your archive.");
mkParentDirs(file);
try (OutputStream out = Files.newOutputStream(file)) {
StreamUtils.copy(zip, out);
}
try {
Files.getFileAttributeView(file, BasicFileAttributeView.class)
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
}
catch (IOException ex) {
// File system does not support setting time attributes. Continue.
}
}

private void mkParentDirs(Path file) throws IOException {
mkDirs(file.getParent());
}

private void mkDirs(Path file) throws IOException {
Files.createDirectories(file);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.jarmode.layertools;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;

import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

/**
* {@link JarStructure} implementation backed by a {@code classpath.idx} file.
*
* @author Stephane Nicoll
*/
class IndexedJarStructure implements JarStructure {

private final Manifest originalManifest;

private final String libLocation;

private final String classesLocation;

private final List<String> classpathEntries;

IndexedJarStructure(Manifest originalManifest, String indexFile) {
this.originalManifest = originalManifest;
this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib");
this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes");
this.classpathEntries = readIndexFile(indexFile);
}

private static String getLocation(Manifest manifest, String attribute) {
String location = getMandatoryAttribute(manifest, attribute);
if (!location.endsWith("/")) {
location = location + "/";
}
return location;
}

private static List<String> readIndexFile(String indexFile) {
String[] lines = Arrays.stream(indexFile.split("\n"))
.map((line) -> line.replace("\r", ""))
.filter(StringUtils::hasText)
.toArray(String[]::new);
List<String> classpathEntries = new ArrayList<>();
for (String line : lines) {
if (line.startsWith("- ")) {
classpathEntries.add(line.substring(3, line.length() - 1));
}
else {
throw new IllegalStateException("Classpath index file is malformed");
}
}
Assert.state(!classpathEntries.isEmpty(), "Empty classpath index file loaded");
return classpathEntries;
}

@Override
public Entry resolve(ZipEntry entry) {
if (entry.isDirectory()) {
return null;
}
String name = entry.getName();
if (this.classpathEntries.contains(name)) {
return new Entry(name, toStructureDependency(name), true);
}
else if (name.startsWith(this.classesLocation)) {
return new Entry(name, name.substring(this.classesLocation.length()), false);
}
return null;
}

@Override
public Manifest createRunJarManifest(UnaryOperator<String> libEntry) {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Name.MANIFEST_VERSION, "1.0");
attributes.put(Name.MAIN_CLASS, getMandatoryAttribute(this.originalManifest, "Start-Class"));
attributes.put(Name.CLASS_PATH,
this.classpathEntries.stream()
.map(this::toStructureDependency)
.map(libEntry)
.collect(Collectors.joining(" ")));
return manifest;
}

@Override
public Manifest createApplicationJarManifest() {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Name.MANIFEST_VERSION, "1.0");
copy(this.originalManifest, manifest, "Implementation-Title");
copy(this.originalManifest, manifest, "Implementation-Version");
return manifest;
}

private static void copy(Manifest left, Manifest right, String attribute) {
String value = left.getMainAttributes().getValue(attribute);
if (value != null) {
right.getMainAttributes().putValue(attribute, value);
}
}

private String toStructureDependency(String libEntryName) {
Assert.state(libEntryName.startsWith(this.libLocation), "Invalid library location " + libEntryName);
return libEntryName.substring(this.libLocation.length());
}

private static String getMandatoryAttribute(Manifest manifest, String attribute) {
String value = manifest.getMainAttributes().getValue(attribute);
if (value == null) {
throw new IllegalStateException("Manifest attribute '" + attribute + "' is mandatory");
}
return value;
}

static IndexedJarStructure get(Context context) {
try {
try (JarFile jarFile = new JarFile(context.getArchiveFile())) {
Manifest manifest = jarFile.getManifest();
String location = getMandatoryAttribute(manifest, "Spring-Boot-Classpath-Index");
ZipEntry entry = jarFile.getEntry(location);
if (entry != null) {
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
return new IndexedJarStructure(manifest, indexFile);
}
}
return null;
}
catch (FileNotFoundException | NoSuchFileException ex) {
return null;
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.jarmode.layertools;

import java.util.function.UnaryOperator;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

/**
* Provide information about a fat jar structure that is meant to be extracted.
*
* @author Stephane Nicoll
*/
interface JarStructure {

/**
* Resolve the specified {@link ZipEntry}, return {@code null} if the entry should not
* be handled.
* @param entry the entry to handle
* @return the resolved {@link Entry}
*/
Entry resolve(ZipEntry entry);

/**
* Create the {@link Manifest} for the {@code run-jar} jar, applying the specified
* operator on each classpath entry.
* @param libEntry the operator to apply on each classpath entry
* @return the manifest to use for the {@code run-jar} jar
*/
Manifest createRunJarManifest(UnaryOperator<String> libEntry);

/**
* Create the {@link Manifest} for the jar that is created with classes and resources.
* @return the manifest to use for the {@code application} jar
*/
Manifest createApplicationJarManifest();

/**
* An entry to handle in the exploded structure.
*
* @param originalLocation the original location
* @param location the relative location
* @param library whether the entry refers to a library or a classpath resource
*/
record Entry(String originalLocation, String location, boolean library) {
}

}
Loading

0 comments on commit 9bf4b2e

Please sign in to comment.