// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. package com.android.tools.r8; import com.android.tools.r8.dex.DexFileReader; import com.android.tools.r8.dex.Segment; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import javax.imageio.ImageIO; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; public class BSPatch { private final static char[] BSDIFF_MAGIC = "BSDIFF40".toCharArray(); private final static int BSDIFF_HEADER_LENGTH = 32; private final ByteBuffer patchInput; private final ByteBuffer oldInput; private final Path output; private final Path dexPath; private InputStream controlStream; private InputStream diffStream; private InputStream extraStream; private long controlBytesRead; private long diffBytesRead; private long extraBytesRead; private int controlBlockLen; private int diffBlockLen; private int extraBlockLen; public static void main(String[] args) { boolean imageMode = args.length > 1 && args[0].equals("-i"); int argOffset = imageMode ? 1 : 0; if (args.length < argOffset + 3) { System.out.println("Usage: [-i] [target dex]"); System.exit(1); } try { new BSPatch(Paths.get(args[argOffset]), Paths.get(args[argOffset + 1]), Paths.get(args[argOffset + 2]), args.length != argOffset + 4 ? null : Paths.get(args[argOffset + 3])) .apply(imageMode); } catch (IOException e) { System.err.println("File I/O error: " + e.toString()); } catch (CompressorException e) { System.err.println("BZIP error: " + e.toString()); } } private BSPatch(Path patchInput, Path oldInput, Path output, Path dexPath) throws IOException { this.patchInput = ByteBuffer.wrap(Files.readAllBytes(patchInput)); this.oldInput = ByteBuffer.wrap(Files.readAllBytes(oldInput)); this.output = output; this.dexPath = dexPath; } public void apply(boolean imageMode) throws CompressorException, IOException { PatchExecutor executor = imageMode ? new ImageExecutor(dexPath) : new FileExecutor(); checkHeader(); setupSegmentsAndOutput(executor); processControl(executor); executor.writeResult(); printStats(); } private int percentOf(long a, long b) { return (int) ((((double) a) / ((double) b)) * 100); } private void printStats() { System.out.println("Size of control block (compressed bytes): " + controlBlockLen); System.out.println("Size of control block (read bytes): " + controlBytesRead); System.out .println("Compression of control block: " + percentOf(controlBlockLen, controlBytesRead)); System.out.println("Size of diff data block (compressed bytes): " + diffBlockLen); System.out.println("Size of diff data block (read bytes): " + diffBytesRead); System.out .println("Compression of diff data block: " + percentOf(diffBlockLen, diffBytesRead)); System.out.println("Size of extra data block (compressed bytes): " + extraBlockLen); System.out.println("Size of extra data block (read bytes): " + extraBytesRead); System.out .println("Compression of extra data block: " + percentOf(extraBlockLen, extraBytesRead)); } private void processControl(PatchExecutor executor) throws IOException { int blockSize; while ((blockSize = readNextControlEntry()) != Integer.MIN_VALUE) { int extraSize = readNextControlEntry(); int advanceOld = readNextControlEntry(); executor.copyDiff(blockSize); executor.copyOld(blockSize); executor.submitBlock(blockSize); executor.copyExtra(extraSize); executor.skipOld(advanceOld); } } private void checkHeader() { for (int i = 0; i < BSDIFF_MAGIC.length; i++) { if (patchInput.get() != BSDIFF_MAGIC[i]) { throw new RuntimeException("Illegal patch, wrong magic!"); } } } private void setupSegmentsAndOutput(PatchExecutor executor) throws CompressorException, IOException { controlBlockLen = readOffset(); diffBlockLen = readOffset(); int newFileSize = readOffset(); extraBlockLen = patchInput.array().length - (BSDIFF_HEADER_LENGTH + controlBlockLen + diffBlockLen); executor.createOutput(newFileSize); controlStream = new BZip2CompressorInputStream( new ByteArrayInputStream(patchInput.array(), BSDIFF_HEADER_LENGTH, controlBlockLen)); diffStream = new BZip2CompressorInputStream( new ByteArrayInputStream(patchInput.array(), BSDIFF_HEADER_LENGTH + controlBlockLen, diffBlockLen)); extraStream = new BZip2CompressorInputStream( new ByteArrayInputStream(patchInput.array(), BSDIFF_HEADER_LENGTH + controlBlockLen + diffBlockLen, extraBlockLen)); } private int readOffset() { byte[] buffer = new byte[8]; patchInput.get(buffer); return decodeOffset(buffer); } private int readNextControlEntry() throws IOException { byte[] buffer = new byte[8]; int read = controlStream.read(buffer); if (read == -1) { return Integer.MIN_VALUE; } controlBytesRead += read; assert read == buffer.length; return decodeOffset(buffer); } private static int decodeOffset(byte[] buffer) { long offset = buffer[7] & 0x7F; for (int i = 6; i >= 0; i--) { offset = (offset << 8) | (((int) buffer[i]) & 0xff); } if ((buffer[7] & 0x80) != 0) { offset = -offset; } assert offset < Integer.MAX_VALUE && offset > Integer.MIN_VALUE; return (int) offset; } private abstract class PatchExecutor { public abstract void createOutput(int newFileSize); public abstract void copyDiff(int blockSize) throws IOException; public abstract void copyOld(int blockSize) throws IOException; public abstract void submitBlock(int blockSize); public abstract void copyExtra(int extraSize) throws IOException; public abstract void skipOld(int advanceOld) throws IOException; public abstract void writeResult() throws IOException; } private class FileExecutor extends PatchExecutor { private ByteBuffer resultBuffer; private byte[] mergeBuffer = null; @Override public void createOutput(int newFileSize) { resultBuffer = ByteBuffer.allocate(newFileSize); } @Override public void copyDiff(int blockSize) throws IOException { assert mergeBuffer == null; mergeBuffer = new byte[blockSize]; int read = diffStream.read(mergeBuffer); diffBytesRead += read; assert read == blockSize; } @Override public void copyOld(int blockSize) throws IOException { assert mergeBuffer != null; assert mergeBuffer.length == blockSize; byte[] data = new byte[blockSize]; oldInput.get(data); for (int i = 0; i < mergeBuffer.length; i++) { mergeBuffer[i] = (byte) ((((int) mergeBuffer[i]) & 0xff) + (((int) data[i]) & 0xff)); } } @Override public void submitBlock(int blockSize) { assert mergeBuffer != null; assert mergeBuffer.length == blockSize; resultBuffer.put(mergeBuffer); mergeBuffer = null; } @Override public void copyExtra(int extraSize) throws IOException { byte[] data = new byte[extraSize]; int read = extraStream.read(data); assert read == extraSize; extraBytesRead += read; resultBuffer.put(data); } @Override public void skipOld(int delta) throws IOException { oldInput.position(oldInput.position() + delta); } @Override public void writeResult() throws IOException { OutputStream outputStream = Files.newOutputStream(output); outputStream.write(resultBuffer.array()); outputStream.close(); } } private class ImageExecutor extends PatchExecutor { private final Path dexPath; BufferedImage image; int position = 0; int width; int height; private ImageExecutor(Path dexPath) { this.dexPath = dexPath; } @Override public void createOutput(int newFileSize) { int root = (int) Math.sqrt(newFileSize); width = newFileSize / root; height = newFileSize / width + 1; image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); } @Override public void copyDiff(int blockSize) throws IOException { byte[] buffer = new byte[blockSize]; int read = diffStream.read(buffer); assert read == blockSize; diffBytesRead += read; for (int i = 0; i < buffer.length; i++) { if (buffer[i] != 0) { int y = (position + i) / width; int x = (position + i) % width; int rgb = image.getRGB(x, y); rgb = rgb | 0xFF0000; image.setRGB(x, y, rgb); } } } @Override public void copyOld(int blockSize) throws IOException { for (int i = 0; i < blockSize; i++) { int y = (position + i) / width; int x = (position + i) % width; int rgb = image.getRGB(x, y); if ((rgb & 0xFF0000) == 0) { rgb = rgb | 0xFF00; } image.setRGB(x, y, rgb); } } @Override public void submitBlock(int blockSize) { position += blockSize; } @Override public void copyExtra(int extraSize) throws IOException { long skipped = extraStream.skip(extraSize); assert skipped == extraSize; extraBytesRead += skipped; for (int i = 0; i < extraSize; i++) { int y = (position + i) / width; int x = (position + i) % width; int rgb = image.getRGB(x, y); rgb = rgb | 0xFF; image.setRGB(x, y, rgb); } position += extraSize; } @Override public void skipOld(int advanceOld) throws IOException { } @Override public void writeResult() throws IOException { if (dexPath != null) { Segment[] segments = DexFileReader.parseMapFrom(dexPath); for (Segment segment : segments) { int y = segment.offset / width; for (int x = 0; x < width; x++) { int val = (x / 10) % 2 == 0 ? 0 : 0xffffff; image.setRGB(x, y, val); } System.out.println(segment); } } ImageIO.write(image, "png", output.toFile()); } } }