package org.getmonero.util.normalizeZip;/* * 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 * * http://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. * * Based on: https://github.com/Zlika/reproducible-build-maven-plugin/blob/master/src/main/java/io/github/zlika/reproducible/ZipStripper.java */ import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileTime; import java.util.*; import java.util.stream.Collectors; import org.apache.commons.compress.archivers.zip.X5455_ExtendedTimestamp; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipExtraField; import org.apache.commons.compress.archivers.zip.ZipFile; /** * Strips non-reproducible data from a ZIP file. * It rebuilds the ZIP file with a predictable order for the zip entries and sets zip entry dates to a fixed value. */ public final class NormalizeZip { public static void main(String[] args) throws Exception { if(args.length!=2) { System.out.println("Arguments: timestamp zip"); System.exit(1); } long timestamp = Long.parseLong(args[0]); File sourceZipFile = Path.of(args[1]).toFile(); File destZipFile = Path.of(args[1]+".tmp").toFile(); if(!sourceZipFile.exists()) { System.out.println("Source zip file does not exist"); System.exit(1); } System.out.println("Normalizing zip " + sourceZipFile.getCanonicalPath() + " to timestamp: " + new Date(timestamp)); NormalizeZip nz = new NormalizeZip(timestamp, true).addFileStripper("META-INF/MANIFEST.MF", new ManifestStripper()); nz.strip(sourceZipFile, destZipFile); if(!sourceZipFile.delete()) System.out.println("Cannot delete source file"); if(!destZipFile.renameTo(sourceZipFile)) System.out.println("Cannot rename temporary zip file"); destZipFile = sourceZipFile; if(!destZipFile.setLastModified(timestamp)) System.out.println("Cannot update zip last modified date"); } /** * Comparator used to sort the files in the ZIP file. * This is mostly an alphabetical order comparator, with the exception that * META-INF/MANIFEST.MF and META-INF/ must be the 2 first entries (if they exist) * because this is required by some tools * (cf. https://github.com/Zlika/reproducible-build-maven-plugin/issues/16). */ private static final Comparator MANIFEST_FILE_SORT_COMPARATOR = new Comparator() { // CHECKSTYLE IGNORE LINE: ReturnCount @Override public int compare(String o1, String o2) { if ("META-INF/MANIFEST.MF".equals(o1)) { return -1; } if ("META-INF/MANIFEST.MF".equals(o2)) { return 1; } if ("META-INF/".equals(o1)) { return -1; } if ("META-INF/".equals(o2)) { return 1; } return o1.compareTo(o2); } }; private final long zipTimestamp; private final boolean fixZipExternalFileAttributes; public NormalizeZip(long zipTimestamp, boolean fixZipExternalFileAttributes) { this.zipTimestamp = zipTimestamp; this.fixZipExternalFileAttributes = fixZipExternalFileAttributes; } private final Map subFilters = new HashMap<>(); private Stripper getSubFilter(String name) { for (Map.Entry filter : subFilters.entrySet()) { if (name.matches(filter.getKey())) { return filter.getValue(); } } return null; } /** * Adds a stripper for a given file in the Zip. * @param filename the name of the file in the Zip (regular expression). * @param stripper the stripper to apply on the file. * @return this object (for method chaining). */ public NormalizeZip addFileStripper(String filename, Stripper stripper) { subFilters.put(filename, stripper); return this; } public void strip(File in, File out) throws IOException { try (final ZipFile zip = new ZipFile(in); final ZipArchiveOutputStream zout = new ZipArchiveOutputStream(out)) { final List sortedNames = sortEntriesByName(zip.getEntries()); for (String name : sortedNames) { final ZipArchiveEntry entry = zip.getEntry(name); // Strip Zip entry final ZipArchiveEntry strippedEntry = filterZipEntry(entry); // Fix external file attributes if required if (in.getName().endsWith(".jar") || in.getName().endsWith(".war")) { fixAttributes(strippedEntry); } // Strip file if required final Stripper stripper = getSubFilter(name); if (stripper != null) { // Unzip entry to temp file final File tmp = File.createTempFile("tmp", null); tmp.deleteOnExit(); Files.copy(zip.getInputStream(entry), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING); final File tmp2 = File.createTempFile("tmp", null); tmp2.deleteOnExit(); stripper.strip(tmp, tmp2); final byte[] fileContent = Files.readAllBytes(tmp2.toPath()); strippedEntry.setSize(fileContent.length); zout.putArchiveEntry(strippedEntry); zout.write(fileContent); zout.closeArchiveEntry(); } else { // Copy the Zip entry as-is zout.addRawArchiveEntry(strippedEntry, zip.getRawInputStream(entry)); } } } } private void fixAttributes(ZipArchiveEntry entry) { if (fixZipExternalFileAttributes) { /* ZIP external file attributes: TTTTsstrwxrwxrwx0000000000ADVSHR ^^^^____________________________ file type (file: 1000 , dir: 0100) ^^^_________________________ setuid, setgid, sticky ^^^^^^^^^________________ Unix permissions ^^^^^^ DOS attributes The argument of setUnixMode() only takes the 2 upper bytes. */ if (entry.isDirectory()) { entry.setUnixMode((0b0100 << 12) + 0755); } else { entry.setUnixMode((0b1000 << 12) + 0644); } } } private List sortEntriesByName(Enumeration entries) { return Collections.list(entries).stream() .map(e -> e.getName()) .sorted(MANIFEST_FILE_SORT_COMPARATOR) .collect(Collectors.toList()); } private ZipArchiveEntry filterZipEntry(ZipArchiveEntry entry) { // Set times entry.setCreationTime(FileTime.fromMillis(zipTimestamp)); entry.setLastAccessTime(FileTime.fromMillis(zipTimestamp)); entry.setLastModifiedTime(FileTime.fromMillis(zipTimestamp)); entry.setTime(zipTimestamp); // Remove extended timestamps for (ZipExtraField field : entry.getExtraFields()) { if (field instanceof X5455_ExtendedTimestamp) { entry.removeExtraField(field.getHeaderId()); } } return entry; } }