Implement range-based pin list

This extends the original pin list generation to support specifying a
range within a file.  If any part of a file is pinned, its local file
header in the APK file is pinned as well.

Test: unzip -q -c signed.apk pinlist.meta | od --endian=big -w8 -t d4 -
Bug: 136040313
Bug: 135953430
Change-Id: I2f6383a47effee4095055ad1f12e5cc701c19f42
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 88f2617..e3870e7 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -240,7 +240,8 @@
         List<CentralDirectoryRecord> inputCdRecords =
                 parseZipCentralDirectory(inputCd, inputZipSections);
 
-        List<Pattern> pinPatterns = extractPinPatterns(inputCdRecords, inputApkLfhSection);
+        List<Hints.PatternWithRange> pinPatterns = extractPinPatterns(
+                inputCdRecords, inputApkLfhSection);
         List<Hints.ByteRange> pinByteRanges = pinPatterns == null ? null : new ArrayList<>();
 
         // Step 3. Obtain a signer engine instance
@@ -371,28 +372,33 @@
 
                 // Output entry's Local File Header + data
                 long outputLocalFileHeaderOffset = outputOffset;
-                long outputLocalFileRecordSize =
+                OutputSizeAndDataOffset outputLfrResult =
                         outputInputJarEntryLfhRecordPreservingDataAlignment(
                                 inputApkLfhSection,
                                 inputLocalFileRecord,
                                 outputApkOut,
                                 outputLocalFileHeaderOffset);
-                outputOffset += outputLocalFileRecordSize;
+                outputOffset += outputLfrResult.outputBytes;
+                long outputDataOffset =
+                        outputLocalFileHeaderOffset + outputLfrResult.dataOffsetBytes;
 
                 if (pinPatterns != null) {
-                    boolean pinThisFile = false;
-                    for (Pattern pinPattern : pinPatterns) {
+                    boolean pinFileHeader = false;
+                    for (Hints.PatternWithRange pinPattern : pinPatterns) {
                         if (pinPattern.matcher(inputCdRecord.getName()).matches()) {
-                            pinThisFile = true;
-                            break;
+                            Hints.ByteRange dataRange =
+                                    new Hints.ByteRange(outputDataOffset, outputOffset);
+                            Hints.ByteRange pinRange =
+                                    pinPattern.ClampToAbsoluteByteRange(dataRange);
+                            if (pinRange != null) {
+                                pinFileHeader = true;
+                                pinByteRanges.add(pinRange);
+                            }
                         }
                     }
-
-                    if (pinThisFile) {
-                        pinByteRanges.add(
-                            new Hints.ByteRange(
-                                outputLocalFileHeaderOffset,
-                                outputOffset));
+                    if (pinFileHeader) {
+                        pinByteRanges.add(new Hints.ByteRange(outputLocalFileHeaderOffset,
+                                                              outputDataOffset));
                     }
                 }
 
@@ -574,7 +580,17 @@
         inspectEntryRequest.done();
     }
 
-    private static long outputInputJarEntryLfhRecordPreservingDataAlignment(
+    private static class OutputSizeAndDataOffset {
+        public long outputBytes;
+        public long dataOffsetBytes;
+
+        public OutputSizeAndDataOffset(long outputBytes, long dataOffsetBytes) {
+            this.outputBytes = outputBytes;
+            this.dataOffsetBytes = dataOffsetBytes;
+        }
+    }
+
+    private static OutputSizeAndDataOffset outputInputJarEntryLfhRecordPreservingDataAlignment(
             DataSource inputLfhSection,
             LocalFileRecord inputRecord,
             DataSink outputLfhSection,
@@ -582,21 +598,27 @@
         long inputOffset = inputRecord.getStartOffsetInArchive();
         if (inputOffset == outputOffset) {
             // This record's data will be aligned same as in the input APK.
-            return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+            return new OutputSizeAndDataOffset(
+                    inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+                    inputRecord.getDataStartOffsetInRecord());
         }
         int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord);
         if ((dataAlignmentMultiple <= 1)
                 || ((inputOffset % dataAlignmentMultiple)
                         == (outputOffset % dataAlignmentMultiple))) {
             // This record's data will be aligned same as in the input APK.
-            return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+            return new OutputSizeAndDataOffset(
+                    inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+                    inputRecord.getDataStartOffsetInRecord());
         }
 
         long inputDataStartOffset = inputOffset + inputRecord.getDataStartOffsetInRecord();
         if ((inputDataStartOffset % dataAlignmentMultiple) != 0) {
             // This record's data is not aligned in the input APK. No need to align it in the
             // output.
-            return inputRecord.outputRecord(inputLfhSection, outputLfhSection);
+            return new OutputSizeAndDataOffset(
+                    inputRecord.outputRecord(inputLfhSection, outputLfhSection),
+                    inputRecord.getDataStartOffsetInRecord());
         }
 
         // This record's data needs to be re-aligned in the output. This is achieved using the
@@ -606,8 +628,11 @@
                         inputRecord.getExtra(),
                         outputOffset + inputRecord.getExtraFieldStartOffsetInsideRecord(),
                         dataAlignmentMultiple);
-        return inputRecord.outputRecordWithModifiedExtra(
-                inputLfhSection, aligningExtra, outputLfhSection);
+        long dataOffset = inputRecord.getDataStartOffsetInRecord() +
+                          aligningExtra.remaining() -
+                          inputRecord.getExtra().remaining();
+        return new OutputSizeAndDataOffset(inputRecord.outputRecordWithModifiedExtra(
+                inputLfhSection, aligningExtra, outputLfhSection), dataOffset);
     }
 
     private static int getInputJarEntryDataAlignmentMultiple(LocalFileRecord entry) {
@@ -794,12 +819,12 @@
      * Return list of pin patterns embedded in the pin pattern asset
      * file.  If no such file, return {@code null}.
      */
-    private static List<Pattern> extractPinPatterns(
+    private static List<Hints.PatternWithRange> extractPinPatterns(
             List<CentralDirectoryRecord> cdRecords, DataSource lhfSection)
                     throws IOException, ApkFormatException {
         CentralDirectoryRecord pinListCdRecord =
                 findCdRecord(cdRecords, Hints.PIN_HINT_ASSET_ZIP_ENTRY_NAME);
-        List<Pattern> pinPatterns = null;
+        List<Hints.PatternWithRange> pinPatterns = null;
         if (pinListCdRecord != null) {
             pinPatterns = new ArrayList<>();
             byte[] patternBlob;
diff --git a/src/main/java/com/android/apksig/Hints.java b/src/main/java/com/android/apksig/Hints.java
index 49ef2b0..4070fa2 100644
--- a/src/main/java/com/android/apksig/Hints.java
+++ b/src/main/java/com/android/apksig/Hints.java
@@ -20,6 +20,7 @@
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public final class Hints {
@@ -47,6 +48,39 @@
         }
     }
 
+    public static final class PatternWithRange {
+        final Pattern pattern;
+        final long offset;
+        final long size;
+
+        public PatternWithRange(String pattern) {
+            this.pattern = Pattern.compile(pattern);
+            this.offset= 0;
+            this.size = Long.MAX_VALUE;
+        }
+
+        public PatternWithRange(String pattern, long offset, long size) {
+            this.pattern = Pattern.compile(pattern);
+            this.offset = offset;
+            this.size = size;
+        }
+
+        public Matcher matcher(CharSequence input) {
+            return this.pattern.matcher(input);
+        }
+
+        public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
+            if (rangeIn.end - rangeIn.start < this.offset) {
+                return null;
+            }
+            long rangeOutStart = rangeIn.start + this.offset;
+            long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
+                                           this.size);
+            return new ByteRange(rangeOutStart,
+                                 rangeOutStart + rangeOutSize);
+        }
+    }
+
     /**
      * Create a blob of bytes that PinnerService understands as a
      * sequence of byte ranges to pin.
@@ -65,13 +99,20 @@
         return bos.toByteArray();
     }
 
-    public static ArrayList<Pattern> parsePinPatterns(byte[] patternBlob) {
-        ArrayList<Pattern> pinPatterns = new ArrayList<>();
+    public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
+        ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
         try {
             for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
                 String line = rawLine.replaceFirst("#.*", "");  // # starts a comment
-                if (!("".equals(line))) {
-                    pinPatterns.add(Pattern.compile(line));
+                String[] fields = line.split(" ");
+                if (fields.length == 1) {
+                    pinPatterns.add(new PatternWithRange(fields[0]));
+                } else if (fields.length == 3) {
+                    long start = Long.parseLong(fields[1]);
+                    long end = Long.parseLong(fields[2]);
+                    pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
+                } else {
+                    throw new AssertionError("bad pin pattern line " + line);
                 }
             }
         } catch (UnsupportedEncodingException ex) {