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 31be283
Show file tree
Hide file tree
Showing 7 changed files with 663 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
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.util.Assert;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;

/**
* Hacking.
*
* @author Stephane Nicoll
*/
public 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;

public 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 entry = zip.getNextEntry();
Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString()
+ "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled");
while (entry != null) {
String location = this.jarStructure.resolveLocation(entry);
if (location != null) {
write(zip, entry, destination, location);
}
entry = zip.getNextEntry();
}
}
// Create the application jar
Path applicationJar = createApplicationJar(destination, application);

// create the run-app.jar
List<String> classpath = new ArrayList<>();
classpath.add(destination.relativize(applicationJar).toString());
classpath.addAll(this.jarStructure.getClasspathEntries());
createRunAppJar(destination, this.jarStructure.getStartClass(), classpath);

}
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, String mainClassName, List<String> classpath) throws IOException {
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.put(Name.MANIFEST_VERSION, "1.0");
attributes.put(Name.MAIN_CLASS, mainClassName);
attributes.put(Name.CLASS_PATH, String.join(" ", classpath));
Path runJar = destination.resolve("run-app.jar");
try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(runJar), manifest)) {
}
}

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,136 @@
/*
* 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.jar.Attributes;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

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

/**
* @author Stephane Nicoll
*/
class IndexedJarStructure implements JarStructure {

/**
* The directory that contains the application classes.
*/
public static final String APPLICATION_DIRECTORY = "application/";

/**
* The directory that contains libraries.
*/
public static final String DEPENDENCIES_DIRECTORY = "dependencies/";

private final String startClass;

private final String libLocation;

private final String classesLocation;

private final List<String> classpathEntries;

IndexedJarStructure(String startClass, String libLocation, String classesLocation, String indexFile) {
this.startClass = startClass;
this.libLocation = (libLocation.endsWith("/") ? libLocation : libLocation + "/");
this.classesLocation = (classesLocation.endsWith("/") ? classesLocation : classesLocation + "/");
this.classpathEntries = readIndexFile(indexFile);
}

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 String getStartClass() {
return this.startClass;
}

@Override
public List<String> getClasspathEntries() {
return this.classpathEntries.stream().map(this::toStructureDependency).toList();
}

@Override
public String resolveLocation(ZipEntry entry) {
if (entry.isDirectory()) {
return null;
}
String name = entry.getName();
if (this.classpathEntries.contains(name)) {
return toStructureDependency(name);
}
else if (name.startsWith(this.classesLocation)) {
return APPLICATION_DIRECTORY + name.substring(this.classesLocation.length());
}
return null;
}

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

static IndexedJarStructure get(Context context) {
try {
try (JarFile jarFile = new JarFile(context.getArchiveFile())) {
Attributes attributes = jarFile.getManifest().getMainAttributes();
String startClass = attributes.getValue("Start-Class");
String libLocation = attributes.getValue("Spring-Boot-Lib");
String classesLocation = attributes.getValue("Spring-Boot-Classes");
String location = attributes.getValue("Spring-Boot-Classpath-Index");
ZipEntry entry = (location != null) ? jarFile.getEntry(location) : null;
if (startClass != null && libLocation != null && classesLocation != null && entry != null) {
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
return new IndexedJarStructure(startClass, libLocation, classesLocation, 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,45 @@
/*
* 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.List;
import java.util.zip.ZipEntry;

/**
* @author Stephane Nicoll
*/
public interface JarStructure {

/**
* Return the class name to use to start the application.
* @return the application class name
*/
String getStartClass();

List<String> getClasspathEntries();

/**
* Resolve the location of the specified entry, or {@code null} if
* the entry should not be handled.
* @param entry the entry to handle
* @return the location of the entry, in a relative form
*/
String resolveLocation(ZipEntry entry);



}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ static List<Command> getCommands(Context context) {
List<Command> commands = new ArrayList<>();
commands.add(new ListCommand(context));
commands.add(new ExtractCommand(context));
commands.add(new ExtractJarCommand(context));
return Collections.unmodifiableList(commands);
}

Expand Down
Loading

0 comments on commit 31be283

Please sign in to comment.