• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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