/*
* 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.cxx;
import static com.facebook.buck.cxx.DebugSectionProperty.COMPRESSED;
import static com.facebook.buck.cxx.DebugSectionProperty.STRINGS;
import static java.nio.channels.FileChannel.MapMode.READ_WRITE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;
import com.facebook.buck.log.Logger;
import com.facebook.buck.util.ByteBufferReplacer;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nonnull;
/**
* This sanitizer works by munging the compiler output to replace paths. Currently, this just
* supports sanitizing the compilation directory that compilers typically embed in binaries when
* including debug sections (e.g. using "-g" with gcc/clang inserts the compilation directory in the
* DW_AT_comp_dir DWARF field).
*/
public class MungingDebugPathSanitizer extends DebugPathSanitizer {
private static final DebugSectionFinder DEBUG_SECTION_FINDER = new DebugSectionFinder();
protected final ImmutableBiMap<Path, Path> other;
private final LoadingCache<Path, ImmutableBiMap<Path, Path>> pathCache =
CacheBuilder.newBuilder()
.softValues()
.build(
new CacheLoader<Path, ImmutableBiMap<Path, Path>>() {
@Override
public ImmutableBiMap<Path, Path> load(@Nonnull Path key) {
return getAllPathsWork(key);
}
});
/**
* @param pathSize fix paths to this size for in-place replacements.
* @param separator the path separator used to fill paths aren't of {@code pathSize} length.
* @param compilationDirectory the desired path to replace the actual compilation directory with.
*/
public MungingDebugPathSanitizer(
int pathSize, char separator, Path compilationDirectory, ImmutableBiMap<Path, Path> other) {
super(separator, pathSize, compilationDirectory);
this.other = other;
}
@Override
public ImmutableMap<String, String> getCompilationEnvironment(
Path workingDir, boolean shouldSanitize) {
// A forced compilation directory is set in the constructor. Now, we can't actually force
// the compiler to embed this into the binary -- all we can do set the PWD environment to
// variations of the actual current working directory (e.g. /actual/dir or
// /actual/dir////). This adjustment serves two purposes:
//
// 1) it makes the compiler's current-directory line directive output agree with its cwd,
// given by getProjectDirectoryRoot. (If PWD and cwd are different names for the same
// directory, the compiler prefers PWD, but we expect cwd for DebugPathSanitizer.)
//
// 2) in the case where we're using post-linkd debug path replacement, we reserve room
// to expand the path later.
return ImmutableMap.of(
"PWD", shouldSanitize ? getExpandedPath(workingDir) : workingDir.toString());
}
@Override
ImmutableList<String> getCompilationFlags() {
return ImmutableList.of();
}
// Construct the replacer, giving the expanded current directory and the desired directory.
// We use ASCII, since all the relevant debug standards we care about (e.g. DWARF) use it.
@Override
public void restoreCompilationDirectory(Path path, Path workingDir) throws IOException {
restore(path, getCompilationDirectoryReplacer(workingDir));
}
/**
* @return a {@link ByteBufferReplacer} suitable for replacing {@code workingDir} with {@code
* compilationDirectory}.
*/
protected ByteBufferReplacer getCompilationDirectoryReplacer(Path workingDir) {
return new ByteBufferReplacer(
ImmutableMap.of(
getExpandedPath(workingDir).getBytes(Charsets.US_ASCII),
getExpandedPath(compilationDirectory).getBytes(Charsets.US_ASCII)));
}
/**
* Run {@code replacer} on all relevant debug sections in {@code buffer}, falling back to
* processing the entire {@code buffer} if the format is unrecognized.
*/
private void restore(ByteBuffer buffer, ByteBufferReplacer replacer) {
// Find the debug sections in the file represented by the buffer.
Optional<ImmutableMap<String, DebugSection>> results = DEBUG_SECTION_FINDER.find(buffer);
// If we were able to recognize the file format and find debug symbols, perform the
// replacement on them. Otherwise, just do a find-and-replace on the whole blob.
if (results.isPresent()) {
for (DebugSection section : results.get().values()) {
// We can't do in-place updates on compressed debug sections.
Preconditions.checkState(!section.properties.contains(COMPRESSED));
if (section.properties.contains(STRINGS)) {
replacer.replace(section.body);
}
}
} else {
replacer.replace(buffer);
}
}
protected void restore(Path path, ByteBufferReplacer replacer) throws IOException {
try (FileChannel channel = FileChannel.open(path, READ, WRITE)) {
MappedByteBuffer buffer = channel.map(READ_WRITE, 0, channel.size());
restore(buffer, replacer);
}
}
@Override
protected ImmutableBiMap<Path, Path> getAllPaths(Optional<Path> workingDir) {
if (!workingDir.isPresent()) {
return other;
}
try {
return pathCache.get(workingDir.get());
} catch (ExecutionException e) {
Logger.get(DebugPathSanitizer.class).error("Problem loading paths into cache", e);
return getAllPathsWork(workingDir.get());
}
}
private ImmutableBiMap<Path, Path> getAllPathsWork(Path workingDir) {
ImmutableBiMap.Builder<Path, Path> builder = ImmutableBiMap.builder();
builder.put(workingDir, compilationDirectory);
builder.putAll(other);
return builder.build();
}
}