/* * Copyright 2015-present Facebook, Inc. * * 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. */ package com.facebook.buck.cxx; import com.facebook.buck.io.FileContentsScrubber; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import com.google.common.primitives.Shorts; import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ReadOnlyBufferException; import java.nio.channels.FileChannel; import java.util.Arrays; public class ObjectFileScrubbers { private static final int GLOBAL_HEADER_SIZE = 8; private static final ImmutableSet<String> SPECIAL_ENTRIES = ImmutableSet.of("/", "//"); public static final byte[] GLOBAL_HEADER = "!<arch>\n".getBytes(Charsets.US_ASCII); public static final byte[] GLOBAL_THIN_HEADER = "!<thin>\n".getBytes(Charsets.US_ASCII); public static final byte[] END_OF_FILE_HEADER_MARKER = {0x60, 0x0A}; public enum PaddingStyle { LEFT, RIGHT, }; private ObjectFileScrubbers() {} private static boolean checkHeader(byte[] header) throws FileContentsScrubber.ScrubException { checkArchive( Arrays.equals(GLOBAL_HEADER, header) || Arrays.equals(GLOBAL_THIN_HEADER, header), "invalid global header"); return Arrays.equals(GLOBAL_THIN_HEADER, header); } public static FileContentsScrubber createDateUidGidScrubber(final PaddingStyle paddingStyle) { return new FileContentsScrubber() { /** * Efficiently modifies the archive backed by the given buffer to remove any non-deterministic * meta-data such as timestamps, UIDs, and GIDs. */ @SuppressWarnings("PMD.AvoidUsingOctalValues") @Override public void scrubFile(FileChannel file) throws IOException, ScrubException { try { ByteBuffer header = ByteBuffer.allocate(GLOBAL_HEADER_SIZE); file.read(header); // Grab the global header chunk and verify it's accurate. header.position(0); byte[] globalHeader = getBytes(header, GLOBAL_HEADER_SIZE); boolean thin = checkHeader(globalHeader); // Iterate over all the file meta-data entries, injecting zero's for timestamp, // UID, and GID. final int entrySize = 16 /* fileName */ + 12 /* file modification time */ + 6 /* owner ID */ + 6 /* group ID */ + 8 /* file mode */ + 10 /* file size */ + 2 /* file magic */; long start = GLOBAL_HEADER_SIZE; ByteBuffer buffer = ByteBuffer.allocate(entrySize); while (start < file.size()) { checkArchive(file.size() - start >= entrySize, "Invalid entry metadata format"); buffer.clear(); file.position(start); int read = file.read(buffer); checkArchive(read == entrySize, "Not all bytes have been read"); buffer.position(0); // position points just past the last byte read, so need to reset String fileName = new String(getBytes(buffer, 16), Charsets.US_ASCII).trim(); // Inject 0's for the non-deterministic meta-data entries. /* File modification timestamp */ putIntAsDecimalString( buffer, 12, ObjectFileCommonModificationDate.COMMON_MODIFICATION_TIME_STAMP, paddingStyle); /* Owner ID */ putIntAsDecimalString(buffer, 6, 0, paddingStyle); /* Group ID */ putIntAsDecimalString(buffer, 6, 0, paddingStyle); /* File mode */ putIntAsOctalString(buffer, 8, 0100644, paddingStyle); long fileSize = getDecimalStringAsLong(buffer, 10); // Lastly, grab the file magic entry and verify it's accurate. byte[] fileMagic = getBytes(buffer, 2); checkArchive(Arrays.equals(END_OF_FILE_HEADER_MARKER, fileMagic), "invalid file magic"); // write the changes buffer.position(0); // position points just past the last byte accessed, need to reset file.position(start); int written = file.write(buffer); checkArchive(written == entrySize, "Not all bytes have been written"); // Skip the file data. start += entrySize; if (!thin || SPECIAL_ENTRIES.contains(fileName)) { start += fileSize + fileSize % 2; } } // Convert any low-level exceptions to `ArchiveExceptions`s. } catch (BufferUnderflowException | ReadOnlyBufferException e) { throw new ScrubException(e.getMessage()); } } }; } public static byte[] getBytes(ByteBuffer buffer, int len) { byte[] bytes = new byte[len]; buffer.get(bytes); return bytes; } public static int getOctalStringAsInt(ByteBuffer buffer, int len) { byte[] bytes = getBytes(buffer, len); String str = new String(bytes, Charsets.US_ASCII); return Integer.parseInt(str.trim(), 8); } public static int getDecimalStringAsInt(ByteBuffer buffer, int len) { byte[] bytes = getBytes(buffer, len); String str = new String(bytes, Charsets.US_ASCII); return Integer.parseInt(str.trim()); } public static long getDecimalStringAsLong(ByteBuffer buffer, int len) { byte[] bytes = getBytes(buffer, len); String str = new String(bytes, Charsets.US_ASCII).trim(); return str.isEmpty() ? 0 : Long.parseLong(str.trim()); } public static long getLittleEndianLong(ByteBuffer buffer) { byte b1 = buffer.get(); byte b2 = buffer.get(); byte b3 = buffer.get(); byte b4 = buffer.get(); byte b5 = buffer.get(); byte b6 = buffer.get(); byte b7 = buffer.get(); byte b8 = buffer.get(); return Longs.fromBytes(b8, b7, b6, b5, b4, b3, b2, b1); } public static int getLittleEndianInt(ByteBuffer buffer) { byte b1 = buffer.get(); byte b2 = buffer.get(); byte b3 = buffer.get(); byte b4 = buffer.get(); return Ints.fromBytes(b4, b3, b2, b1); } public static short getLittleEndianShort(ByteBuffer buffer) { byte b1 = buffer.get(); byte b2 = buffer.get(); return Shorts.fromBytes(b2, b1); } public static String getAsciiString(ByteBuffer buffer) { int position = buffer.position(); int length = 0; do { length++; } while (buffer.get() != 0x00); byte[] bytes = new byte[length - 1]; buffer.position(position); buffer.get(bytes, 0, length - 1); return new String(bytes, Charsets.US_ASCII); } private static void putSpaceLeftPaddedString(ByteBuffer buffer, int len, String value) { Preconditions.checkState(value.length() <= len); value = Strings.padStart(value, len, ' '); buffer.put(value.getBytes(Charsets.US_ASCII)); } private static void putSpaceRightPaddedString(ByteBuffer buffer, int len, String value) { Preconditions.checkState(value.length() <= len); value = Strings.padEnd(value, len, ' '); buffer.put(value.getBytes(Charsets.US_ASCII)); } public static void putBytes(ByteBuffer buffer, byte[] bytes) { buffer.put(bytes); } public static void putIntAsOctalString( ByteBuffer buffer, int len, int value, PaddingStyle paddingStyle) { if (paddingStyle == PaddingStyle.LEFT) { putSpaceLeftPaddedString(buffer, len, String.format("0%o", value)); } else { putSpaceRightPaddedString(buffer, len, String.format("0%o", value)); } } public static void putIntAsDecimalString( ByteBuffer buffer, int len, int value, PaddingStyle paddingStyle) { if (paddingStyle == PaddingStyle.LEFT) { putSpaceLeftPaddedString(buffer, len, String.format("%d", value)); } else { putSpaceRightPaddedString(buffer, len, String.format("%d", value)); } } public static void putLittleEndianLong(ByteBuffer buffer, long value) { byte[] bytes = Longs.toByteArray(value); byte[] flipped = { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0] }; buffer.put(flipped); } public static void putLittleEndianInt(ByteBuffer buffer, int value) { byte[] bytes = Ints.toByteArray(value); byte[] flipped = {bytes[3], bytes[2], bytes[1], bytes[0]}; buffer.put(flipped); } public static void putAsciiString(ByteBuffer buffer, String string) { byte[] bytes = string.getBytes(Charsets.US_ASCII); buffer.put(bytes); buffer.put((byte) 0x00); } public static void checkArchive(boolean expression, String msg) throws FileContentsScrubber.ScrubException { if (!expression) { throw new FileContentsScrubber.ScrubException(msg); } } }