blob: 758d7139aa3a72e232b7a9d6bb9fd643e89eb3d6 [file] [log] [blame]
/*
* Copyright 2014-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.apple.clang;
import static java.lang.Math.max;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Header maps are essentially hash maps from strings to paths (coded as two strings: a prefix and
* a suffix).
* <p>
* This class provides support for reading and generating clang header maps.
* No spec is available but we conform to the
* <a href="http://clang.llvm.org/doxygen/HeaderMap_8h_source.html">reader class defined in the
* Clang documentation</a>.
* <p>
* Note: currently we don't support offsets greater than MAX_SIGNED_INT.
*/
public class HeaderMap {
private static final double MAX_LOAD_FACTOR = 0.75;
/**
* Bucket in the hashtable that is a {@link HeaderMap}.
* Note: This notion of bucket is slightly more abstract than the one on disk (string offsets
* being already swapped/shifted).
*/
private static class Bucket {
/** Offset of the key string into stringBytes. */
final int key;
/** Offset of the value prefix into stringBytes. */
final int prefix;
/** Offset of the value suffix into stringBytes. */
final int suffix;
Bucket(int key, int prefix, int suffix) {
this.key = key;
this.prefix = prefix;
this.suffix = suffix;
}
}
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/** Number of entries in the string table. */
private int numEntries;
/** Number of buckets (always a power of 2). */
private int numBuckets;
/** Length of longest result path (excluding {@code null}). */
private int maxValueLength;
// data containers
private Bucket[] buckets;
private byte[] stringBytes; // actually chars to make debugging easier
private int stringBytesActualLength;
/** Map to help share strings. */
private Map<String, Integer> addedStrings;
private HeaderMap(int numBuckets, int stringBytesLength) {
Preconditions.checkArgument(numBuckets > 0, "The number of buckets must be greater than 0");
Preconditions.checkArgument(
stringBytesLength > 0,
"The size of the string array must be greater than 0");
Preconditions.checkArgument(
(numBuckets & (numBuckets - 1)) == 0,
"The number of buckets must be a power of 2");
this.numEntries = 0;
this.numBuckets = numBuckets;
this.maxValueLength = 0;
this.buckets = new Bucket[numBuckets];
this.stringBytes = new byte[stringBytesLength];
this.stringBytesActualLength = 0;
this.addedStrings = Maps.newHashMap();
}
public int getNumEntries() {
return numEntries;
}
public int getNumBuckets() {
return numBuckets;
}
public int getMaxValueLength() {
return maxValueLength;
}
public interface HeaderMapVisitor {
void apply(String str, String prefix, String suffix);
}
public void visit(HeaderMapVisitor visitor) {
for (int i = 0; i < numBuckets; i++) {
Bucket bucket = buckets[i];
if (bucket != null) {
visitor.apply(
Preconditions.checkNotNull(getString(bucket.key)),
Preconditions.checkNotNull(getString(bucket.prefix)),
Preconditions.checkNotNull(getString(bucket.suffix)));
}
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
print(builder);
return builder.toString();
}
public void print(final Appendable stream) {
visit(new HeaderMapVisitor() {
@Override
public void apply(String str, String prefix, String suffix) {
try {
stream.append("\"");
stream.append(str);
stream.append("\" -> \"");
stream.append(prefix);
stream.append(suffix);
stream.append("\"\n");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
@Nullable
public String lookup(String str) {
int hash0 = hashKey(str) & (numBuckets - 1);
int hash = hash0;
while (true) {
Bucket bucket = buckets[hash];
if (bucket == null) {
return null;
}
if (str.equalsIgnoreCase(getString(bucket.key))) {
return getString(bucket.prefix) + getString(bucket.suffix);
}
hash = (hash + 1) & (numBuckets - 1);
if (hash == hash0) {
return null;
}
}
}
// --------- I/O methods ----------
private static final int HEADER_MAGIC = ('h' << 24) | ('m' << 16) | ('a' << 8) | 'p';
private static final short HEADER_VERSION = 1;
private static final short HEADER_RESERVED = 0;
private static final int EMPTY_BUCKET_KEY = 0;
private static final int HEADER_SIZE = 24;
private static final int BUCKET_SIZE = 12;
@Nullable
public static HeaderMap deserialize(byte[] bytes) {
HeaderMap hmap = new HeaderMap(1, 1);
if (hmap.processBytes(bytes)) {
return hmap;
} else {
return null;
}
}
@Nullable
public static HeaderMap deserialize(ByteBuffer buffer) {
HeaderMap hmap = new HeaderMap(1, 1);
if (hmap.processBuffer(buffer)) {
return hmap;
} else {
return null;
}
}
public static HeaderMap loadFromFile(File hmapFile) throws IOException {
HeaderMap map;
try (FileInputStream inputStream = new FileInputStream(hmapFile)) {
FileChannel fileChannel = inputStream.getChannel();
ByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, hmapFile.length());
map = HeaderMap.deserialize(buffer);
if (map == null) {
throw new IOException("Error while parsing header map " + hmapFile.toString());
}
}
return map;
}
private boolean processBytes(byte[] bytes) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
return processBuffer(buffer);
}
private boolean processBuffer(ByteBuffer buffer) {
buffer.order(ByteOrder.BIG_ENDIAN);
if (buffer.getInt(0) != HEADER_MAGIC) {
buffer.order(ByteOrder.LITTLE_ENDIAN);
}
int magic = buffer.getInt();
short version = buffer.getShort();
if (magic != HEADER_MAGIC || version != HEADER_VERSION) {
return false;
}
/* reserved */ buffer.getShort();
int stringOffset = buffer.getInt();
numEntries = buffer.getInt();
numBuckets = buffer.getInt();
maxValueLength = buffer.getInt();
// strings can be anywhere after the end of the buckets
stringBytesActualLength = buffer.capacity() - HEADER_SIZE - numBuckets * BUCKET_SIZE;
buckets = new Bucket[numBuckets];
int actualOffset = HEADER_SIZE + numBuckets * BUCKET_SIZE - stringOffset;
// actualOffset should always be positive since the index EMPTY_BUCKET_KEY=0 is reserved
for (int i = 0; i < numBuckets; i++) {
int keyRawOffset = buffer.getInt();
int prefixRawOffset = buffer.getInt();
int suffixRawOffset = buffer.getInt();
if (keyRawOffset == EMPTY_BUCKET_KEY) {
buckets[i] = null;
} else {
// we can subtract 1 value because index 0 is EMPTY_BUCKET_KEY
buckets[i] = new Bucket(
keyRawOffset - actualOffset,
prefixRawOffset - actualOffset,
suffixRawOffset - actualOffset);
}
}
// anything else is string
stringBytes = new byte[stringBytesActualLength];
buffer.get(stringBytes);
return true;
}
public int getRequiredBufferCapacity() {
return HEADER_SIZE + numBuckets * BUCKET_SIZE + stringBytesActualLength;
}
public byte[] getBytes() {
return getBytes(ByteOrder.BIG_ENDIAN);
}
public byte[] getBytes(ByteOrder bo) {
byte[] bytes = new byte[getRequiredBufferCapacity()];
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(bo);
serialize(buffer);
return bytes;
}
public void serialize(ByteBuffer buffer) {
int actualOffset = 1; // must be greater than EMPTY_BUCKET_KEY
buffer.putInt(HEADER_MAGIC);
buffer.putShort(HEADER_VERSION);
buffer.putShort(HEADER_RESERVED);
buffer.putInt(HEADER_SIZE + numBuckets * BUCKET_SIZE - actualOffset);
buffer.putInt(numEntries);
buffer.putInt(numBuckets);
buffer.putInt(maxValueLength);
for (int i = 0; i < numBuckets; i++) {
Bucket bucket = buckets[i];
if (bucket == null) {
buffer.putInt(EMPTY_BUCKET_KEY);
buffer.putInt(0);
buffer.putInt(0);
} else {
buffer.putInt(bucket.key + actualOffset);
buffer.putInt(bucket.prefix + actualOffset);
buffer.putInt(bucket.suffix + actualOffset);
}
}
buffer.put(stringBytes, 0, stringBytesActualLength);
}
// -------------- Builder class ----------------
public static Builder builder() {
return new Builder();
}
public static class Builder {
static final int DEFAULT_NUM_BUCKETS = 256;
static final int DEFAULT_STRING_BYTES_LENGTH = 256;
HeaderMap headerMap;
public Builder() {
this.headerMap = new HeaderMap(DEFAULT_NUM_BUCKETS, DEFAULT_STRING_BYTES_LENGTH);
}
public synchronized boolean add(String key, String prefix, String suffix) {
AddResult result = headerMap.add(key, prefix, suffix);
while (result == AddResult.FAILURE_FULL) {
// the table is full, let's start all over again with a doubled number of bucket
// and (optimization) the same size of string bytes
final HeaderMap newHeaderMap = new HeaderMap(
headerMap.numBuckets * 2,
headerMap.stringBytes.length);
headerMap.visit(
new HeaderMapVisitor() {
@Override
public void apply(String str, String prefix, String suffix) {
AddResult copying = newHeaderMap.add(str, prefix, suffix);
assert (copying == AddResult.OK);
}
});
headerMap = newHeaderMap;
result = headerMap.add(key, prefix, suffix);
}
return (result == AddResult.OK);
}
public static String[] splitPath(Path path) {
String[] result = new String[2];
if (path.getNameCount() < 2) {
result[0] = "";
result[1] = path.toString();
} else {
result[0] = path.getParent().toString() + "/";
result[1] = path.getFileName().toString();
}
return result;
}
public boolean add(String key, Path path) {
String[] s = splitPath(path);
return add(key, s[0], s[1]);
}
public synchronized HeaderMap build() {
return headerMap;
}
}
// ------------- Internals -----------
private static int hashKey(String str) {
// ASCII lowercase is part of the format.
// UTF8 is the standard filesystem charset.
return hashKey(Ascii.toLowerCase(str).getBytes(DEFAULT_CHARSET));
}
private static int hashKey(byte[] str) {
int key = 0;
for (byte c : str) {
key += c * 13;
}
return key;
}
private enum AddResult {
OK,
FAILURE_FULL,
FAILURE_ALREADY_PRESENT,
}
@SuppressWarnings("PMD.UnusedPrivateMethod") // PMD has a bad heuristic here.
private AddResult add(String str, String prefix, String suffix) {
if ((numEntries + 1) / (double) numBuckets > MAX_LOAD_FACTOR) {
return AddResult.FAILURE_FULL;
}
int hash0 = hashKey(str) & (numBuckets - 1);
int hash = hash0;
while (true) {
Bucket bucket = buckets[hash];
if (bucket == null) {
bucket = new Bucket(
addString(str),
addString(prefix),
addString(suffix));
buckets[hash] = bucket;
numEntries++;
maxValueLength = max(maxValueLength, prefix.length() + suffix.length());
return AddResult.OK;
}
if (str.equalsIgnoreCase(getString(bucket.key))) {
return AddResult.FAILURE_ALREADY_PRESENT;
}
hash = (hash + 1) & (numBuckets - 1);
if (hash == hash0) {
return AddResult.FAILURE_FULL;
}
}
}
@Nullable
private String getString(int offset) {
Preconditions.checkArgument(offset >= 0 && offset <= stringBytesActualLength);
StringBuffer buffer = new StringBuffer();
byte b = 0;
while ((offset < stringBytesActualLength) && ((b = stringBytes[offset]) != 0)) {
buffer.append((char) b);
offset++;
}
if (offset == stringBytesActualLength) {
// We reached the end of the array without finding a 0.
return null;
}
return buffer.toString();
}
private void putStringByte(byte b) {
if (stringBytesActualLength == stringBytes.length) {
byte[] newBytes = new byte[stringBytes.length * 2];
System.arraycopy(stringBytes, 0, newBytes, 0, stringBytesActualLength);
stringBytes = newBytes;
}
stringBytes[stringBytesActualLength] = b;
stringBytesActualLength++;
}
private int addString(String str) {
Integer existingOffset = addedStrings.get(str);
if (existingOffset != null) {
return existingOffset.intValue();
}
int offset = stringBytesActualLength;
for (byte b : str.getBytes(DEFAULT_CHARSET)) {
putStringByte(b);
}
putStringByte((byte) 0);
addedStrings.put(str, offset);
return offset;
}
}