/*
* Copyright 2016-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.bsd;
import com.facebook.buck.charset.NulTerminatedCharsetDecoder;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class UnixArchive {
private static final byte[] EXPECTED_GLOBAL_HEADER =
"!<arch>\n".getBytes(StandardCharsets.US_ASCII);
private static final byte[] ENTRY_MARKER = "#1/".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_OF_HEADER_MAGIC = {0x60, 0x0A};
private static final int LENGTH_OF_FILENAME_SIZE = 13;
private static final int MODIFICATION_TIME_SIZE = 12;
private static final int OWNER_ID_SIZE = 6;
private static final int GROUP_ID_SIZE = 6;
private static final int FILE_MODE_SIZE = 8;
private static final int FILE_AND_FILENAME_SIZE = 10;
private static final int END_OF_HEADER_MAGIC_SIZE = END_OF_HEADER_MAGIC.length;
private final FileChannel fileChannel;
private final ByteBuffer buffer;
private final Supplier<ImmutableList<UnixArchiveEntry>> entries;
private final NulTerminatedCharsetDecoder nulTerminatedCharsetDecoder;
public UnixArchive(
FileChannel fileChannel, NulTerminatedCharsetDecoder nulTerminatedCharsetDecoder)
throws IOException {
this.fileChannel = fileChannel;
this.buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
if (!checkHeader(buffer)) {
throw new IOException("Archive file has unexpected header");
}
this.entries = Suppliers.memoize(this::loadEntries);
this.nulTerminatedCharsetDecoder = nulTerminatedCharsetDecoder;
}
public static boolean checkHeader(ByteBuffer byteBuffer) {
byte[] header = new byte[EXPECTED_GLOBAL_HEADER.length];
byteBuffer.get(header, 0, header.length);
return Arrays.equals(EXPECTED_GLOBAL_HEADER, header);
}
public ImmutableList<UnixArchiveEntry> getEntries() {
return entries.get();
}
public MappedByteBuffer getMapForEntry(UnixArchiveEntry entry) throws IOException {
long start = entry.getFileOffset();
long len = entry.getFileSize();
return fileChannel.map(FileChannel.MapMode.READ_WRITE, start, len);
}
public void close() throws IOException {
fileChannel.close();
}
@SuppressWarnings("PMD.PrematureDeclaration")
private ImmutableList<UnixArchiveEntry> loadEntries() {
int offset = EXPECTED_GLOBAL_HEADER.length;
int headerOffset = offset;
ImmutableList.Builder<UnixArchiveEntry> builder = ImmutableList.builder();
CharsetDecoder decoder = StandardCharsets.US_ASCII.newDecoder();
while (true) {
buffer.position(offset);
byte[] markerBytes = new byte[ENTRY_MARKER.length];
buffer.get(markerBytes, 0, markerBytes.length);
if (!Arrays.equals(markerBytes, ENTRY_MARKER)) {
throw new HumanReadableException("Unknown entry marker");
}
int filenameLength = getIntFromStringAtRange(LENGTH_OF_FILENAME_SIZE, decoder);
long fileModification = getLongFromStringAtRange(MODIFICATION_TIME_SIZE, decoder);
int ownerId = getIntFromStringAtRange(OWNER_ID_SIZE, decoder);
int groupId = getIntFromStringAtRange(GROUP_ID_SIZE, decoder);
int fileMode = getIntFromStringAtRange(FILE_MODE_SIZE, decoder);
int fileAndFilenameSize = getIntFromStringAtRange(FILE_AND_FILENAME_SIZE, decoder);
byte[] magic = new byte[END_OF_HEADER_MAGIC_SIZE];
buffer.get(magic, 0, magic.length);
if (!Arrays.equals(magic, END_OF_HEADER_MAGIC)) {
throw new HumanReadableException("Unknown file magic");
}
long fileSizeWithoutFilename = fileAndFilenameSize - filenameLength;
offset = buffer.position();
String filename;
try {
filename = nulTerminatedCharsetDecoder.decodeString(buffer);
} catch (CharacterCodingException e) {
throw new HumanReadableException(
e, "Unable to read filename from buffer starting at %d", offset);
}
offset += filenameLength;
builder.add(
UnixArchiveEntry.of(
filenameLength,
fileModification,
ownerId,
groupId,
fileMode,
fileSizeWithoutFilename,
filename,
headerOffset,
offset - headerOffset,
offset));
offset += fileSizeWithoutFilename;
if (offset == buffer.capacity()) {
break;
}
}
return builder.build();
}
@VisibleForTesting
static String readStringWithLength(ByteBuffer buffer, int length, CharsetDecoder charsetDecoder)
throws CharacterCodingException {
int oldLimit = buffer.limit();
buffer.limit(buffer.position() + length);
charsetDecoder.reset();
String result = charsetDecoder.decode(buffer).toString();
buffer.limit(oldLimit);
return result;
}
private int getIntFromStringAtRange(int len, CharsetDecoder decoder) {
String filenameLengthString;
int offset = buffer.position();
try {
filenameLengthString = readStringWithLength(buffer, len, decoder);
} catch (CharacterCodingException e) {
throw new HumanReadableException(
e, "Unable to read int from buffer (range %d..%d)", offset, offset + len);
}
return Integer.parseInt(filenameLengthString.trim());
}
private long getLongFromStringAtRange(int len, CharsetDecoder decoder) {
String filenameLengthString;
int offset = buffer.position();
try {
filenameLengthString = readStringWithLength(buffer, len, decoder);
} catch (CharacterCodingException e) {
throw new HumanReadableException(
e, "Unable to read long from buffer (range %d..%d)", offset, offset + len);
}
return Long.parseLong(filenameLengthString.trim());
}
}