1 // Copyright 2022 Code Intelligence GmbH 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.code_intelligence.jazzer.tools; 16 17 import static java.util.Collections.unmodifiableMap; 18 import static java.util.stream.Collectors.joining; 19 import static java.util.stream.Collectors.mapping; 20 import static java.util.stream.Collectors.partitioningBy; 21 import static java.util.stream.Collectors.toList; 22 23 import java.io.IOException; 24 import java.io.UncheckedIOException; 25 import java.net.URI; 26 import java.net.URISyntaxException; 27 import java.nio.file.FileSystem; 28 import java.nio.file.FileSystems; 29 import java.nio.file.Files; 30 import java.nio.file.Path; 31 import java.nio.file.PathMatcher; 32 import java.nio.file.Paths; 33 import java.util.AbstractMap.SimpleEntry; 34 import java.util.Arrays; 35 import java.util.Comparator; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.TimeZone; 40 import java.util.stream.IntStream; 41 import java.util.stream.Stream; 42 43 public class JarStripper { 44 private static final Map<String, String> ZIP_FS_PROPERTIES = new HashMap<>(); 45 static { 46 // We copy the input to the output path before modifying, so don't try to create a new file at 47 // that path if something went wrong. 48 ZIP_FS_PROPERTIES.put("create", "false"); 49 } 50 main(String[] args)51 public static void main(String[] args) { 52 if (args.length < 2) { 53 System.err.println( 54 "Hermetically removes files and directories from .jar files by relative paths."); 55 System.err.println("Usage: in.jar out.jar [[+]path]..."); 56 System.exit(1); 57 } 58 59 Path inFile = Paths.get(args[0]); 60 Path outFile = Paths.get(args[1]); 61 Map<Boolean, List<String>> rawPaths = unmodifiableMap( 62 Arrays.stream(args) 63 .skip(2) 64 .map(arg -> { 65 if (arg.startsWith("+")) { 66 return new SimpleEntry<>(true, arg.substring(1)); 67 } else { 68 return new SimpleEntry<>(false, arg); 69 } 70 }) 71 .collect(partitioningBy(e -> e.getKey(), mapping(e -> e.getValue(), toList())))); 72 73 try { 74 Files.copy(inFile, outFile); 75 if (!outFile.toFile().setWritable(true)) { 76 System.err.printf("Failed to make %s writable", outFile); 77 System.exit(1); 78 } 79 } catch (IOException e) { 80 e.printStackTrace(); 81 System.exit(1); 82 } 83 84 URI outUri = null; 85 try { 86 outUri = new URI("jar", outFile.toUri().toString(), null); 87 } catch (URISyntaxException e) { 88 e.printStackTrace(); 89 System.exit(1); 90 } 91 92 // Ensure that the ZipFileSystem uses a system-independent time zone for mtimes. 93 // https://github.com/openjdk/jdk/blob/4d64076058a4ec5df101b06572195ed5fdee6f64/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java#L241 94 TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 95 96 try (FileSystem zipFs = FileSystems.newFileSystem(outUri, ZIP_FS_PROPERTIES)) { 97 PathMatcher pathsToDelete = toPathMatcher(zipFs, rawPaths.get(false), false); 98 PathMatcher pathsToKeep = toPathMatcher(zipFs, rawPaths.get(true), true); 99 try (Stream<Path> walk = Files.walk(zipFs.getPath(""))) { 100 walk.sorted(Comparator.reverseOrder()) 101 .filter(path 102 -> (pathsToKeep != null && !pathsToKeep.matches(path)) 103 || (pathsToDelete != null && pathsToDelete.matches(path))) 104 .forEach(path -> { 105 try { 106 Files.delete(path); 107 } catch (IOException e) { 108 throw new UncheckedIOException(e); 109 } 110 }); 111 } 112 } catch (Throwable e) { 113 Throwable throwable = e; 114 if (throwable instanceof UncheckedIOException) { 115 throwable = throwable.getCause(); 116 } 117 throwable.printStackTrace(); 118 System.exit(1); 119 } 120 } 121 toPathMatcher(FileSystem fs, List<String> paths, boolean keep)122 private static PathMatcher toPathMatcher(FileSystem fs, List<String> paths, boolean keep) { 123 if (paths.isEmpty()) { 124 return null; 125 } 126 return fs.getPathMatcher(String.format("glob:{%s}", 127 paths.stream() 128 .flatMap(pattern -> keep ? toKeepGlobs(pattern) : toRemoveGlobs(pattern)) 129 .collect(joining(",")))); 130 } 131 toRemoveGlobs(String path)132 private static Stream<String> toRemoveGlobs(String path) { 133 if (path.endsWith("/**")) { 134 // When removing all contents of a directory, also remove the directory itself. 135 return Stream.of(path, path.substring(0, path.length() - "/**".length())); 136 } else { 137 return Stream.of(path); 138 } 139 } 140 toKeepGlobs(String path)141 private static Stream<String> toKeepGlobs(String path) { 142 // When keeping something, also keep all parents. 143 String[] segments = path.split("/"); 144 return Stream.concat(Stream.of(path), 145 IntStream.range(0, segments.length) 146 .mapToObj(i -> Arrays.stream(segments).limit(i).collect(joining("/")))); 147 } 148 } 149