Add tests for GetNamedDests() API.
authorTom Sepez <tsepez@chromium.org>
Wed, 21 Jan 2015 22:38:33 +0000 (14:38 -0800)
committerTom Sepez <tsepez@chromium.org>
Wed, 21 Jan 2015 22:38:33 +0000 (14:38 -0800)
Follow-on work from patch at https://codereview.chromium.org/845643008. This
incorporates that patch, plus adds tests for it and similar conditions.  A few
changes are introduced to correct expected behaviour.

This also incoprorates Deepak's fix in https://codereview.chromium.org/845643008/

This incorporates a formerly unlanded tool to generate small valid PDF
files from template input, which is needed to cover all the error cases
here. See https://codereview.chromium.org/791993006/

BUG=450133
R=bo_xu@foxitsoftware.com

Review URL: https://codereview.chromium.org/837723009

fpdfsdk/src/fpdfview.cpp
fpdfsdk/src/fpdfview_embeddertest.cpp
testing/resources/named_dests.in [new file with mode: 0644]
testing/resources/named_dests.pdf [new file with mode: 0644]
testing/tools/fixup_pdf_template.py [new file with mode: 0755]

index 3f6bdd9..fba5d65 100644 (file)
@@ -848,8 +848,10 @@ DLLEXPORT FPDF_DEST STDCALL FPDF_GetNamedDest(FPDF_DOCUMENT document, int index,
         pDestObj = nameTree.LookupValue(index, bsName);
     }
     if (!pDestObj) return NULL;
-    if (pDestObj->GetType() == PDFOBJ_DICTIONARY)
+    if (pDestObj->GetType() == PDFOBJ_DICTIONARY) {
         pDestObj = ((CPDF_Dictionary*)pDestObj)->GetArray(FX_BSTRC("D"));
+        if (!pDestObj) return NULL;
+    }
     if (pDestObj->GetType() != PDFOBJ_ARRAY) return NULL;
     CFX_WideString wsName = PDF_DecodeText(bsName);
     CFX_ByteString utf16Name = wsName.UTF16LE_Encode();
@@ -858,6 +860,7 @@ DLLEXPORT FPDF_DEST STDCALL FPDF_GetNamedDest(FPDF_DOCUMENT document, int index,
         buflen = len;
     } else if (buflen >= len) {
         memcpy(buffer, utf16Name.c_str(), len);
+        buflen = len;
     } else {
         buflen = -1;
     }
index 94a5109..55c159a 100644 (file)
@@ -2,7 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <limits>
+#include <string>
+
 #include "../../testing/embedder_test.h"
+#include "../../fpdfsdk/include/fpdfview.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 class FPDFViewEmbeddertest : public EmbedderTest {
@@ -40,3 +44,99 @@ TEST_F(FPDFViewEmbeddertest, ViewerRef) {
   EXPECT_EQ(DuplexUndefined, FPDF_VIEWERREF_GetDuplex(document()));
 }
 
+TEST_F(FPDFViewEmbeddertest, NamedDests) {
+  EXPECT_TRUE(OpenDocument("testing/resources/named_dests.pdf"));
+  long buffer_size;
+  char fixed_buffer[512];
+  FPDF_DEST dest;
+
+  // Query the size of the first item.
+  buffer_size = 2000000; // Absurdly large, check not used for this case.
+  dest = FPDF_GetNamedDest(document(), 0, nullptr, buffer_size);
+  EXPECT_NE(nullptr, dest);
+  EXPECT_EQ(12u, buffer_size);
+
+  // Try to retrieve the first item with too small a buffer.
+  buffer_size = 10;
+  dest = FPDF_GetNamedDest(document(), 0, fixed_buffer, buffer_size);
+  EXPECT_NE(nullptr, dest);
+  EXPECT_EQ(-1, buffer_size);
+
+  // Try to retrieve the first item with correctly sized buffer. Item is
+  // taken from Dests NameTree in named_dests.pdf.
+  buffer_size = 12;
+  dest = FPDF_GetNamedDest(document(), 0, fixed_buffer, buffer_size);
+  EXPECT_NE(nullptr, dest);
+  EXPECT_EQ(12u, buffer_size);
+  EXPECT_EQ(std::string("F\0i\0r\0s\0t\0\0\0", 12),
+            std::string(fixed_buffer, buffer_size));
+
+  // Try to retrieve the second item with ample buffer. Item is taken
+  // from Dests NameTree but has a sub-dictionary in named_dests.pdf.
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), 1, fixed_buffer, buffer_size);
+  EXPECT_NE(nullptr, dest);
+  EXPECT_EQ(10u, buffer_size);
+  EXPECT_EQ(std::string("N\0e\0x\0t\0\0\0", 10),
+            std::string(fixed_buffer, buffer_size));
+
+  // Try to retrieve third item with ample buffer. Item is taken
+  // from Dests NameTree but has a bad sub-dictionary in named_dests.pdf.
+  // in named_dests.pdf).
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), 2, fixed_buffer, buffer_size);
+  EXPECT_EQ(nullptr, dest);
+  EXPECT_EQ(sizeof(fixed_buffer), buffer_size);  // unmodified.
+
+  // Try to retrieve the forth item with ample buffer. Item is taken
+  // from Dests NameTree but has a vale of the wrong type in named_dests.pdf.
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), 3, fixed_buffer, buffer_size);
+  EXPECT_EQ(nullptr, dest);
+  EXPECT_EQ(sizeof(fixed_buffer), buffer_size);  // unmodified.
+
+  // Try to retrieve fifth item with ample buffer. Item taken from the
+  // old-style Dests dictionary object in named_dests.pdf.
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), 4, fixed_buffer, buffer_size);
+  EXPECT_NE(nullptr, dest);
+  EXPECT_EQ(30u, buffer_size);
+  EXPECT_EQ(
+      std::string("F\0i\0r\0s\0t\0A\0l\0t\0e\0r\0n\0a\0t\0e\0\0\0", 30),
+      std::string(fixed_buffer, buffer_size));
+
+  // Try to retrieve sixth item with ample buffer. Item istaken from the
+  // old-style Dests dictionary object but has a sub-dictionary in
+  // named_dests.pdf.
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), 5, fixed_buffer, buffer_size);
+  EXPECT_NE(nullptr, dest);
+  EXPECT_EQ(28u, buffer_size);
+  EXPECT_EQ(
+      std::string("L\0a\0s\0t\0A\0l\0t\0e\0r\0n\0a\0t\0e\0\0\0", 28),
+      std::string(fixed_buffer, buffer_size));
+
+  // Try to retrieve non-existent item with ample buffer.
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), 6, fixed_buffer, buffer_size);
+  EXPECT_EQ(nullptr, dest);
+  EXPECT_EQ(sizeof(fixed_buffer), buffer_size);  // unmodified.
+
+  // Try to underflow/overflow the integer index.
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), std::numeric_limits<int>::max(),
+                           fixed_buffer, buffer_size);
+  EXPECT_EQ(nullptr, dest);
+  EXPECT_EQ(sizeof(fixed_buffer), buffer_size);  // unmodified.
+
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), std::numeric_limits<int>::min(),
+                           fixed_buffer, buffer_size);
+  EXPECT_EQ(nullptr, dest);
+  EXPECT_EQ(sizeof(fixed_buffer), buffer_size);  // unmodified.
+
+  buffer_size = sizeof(fixed_buffer);
+  dest = FPDF_GetNamedDest(document(), -1, fixed_buffer, buffer_size);
+  EXPECT_EQ(nullptr, dest);
+  EXPECT_EQ(sizeof(fixed_buffer), buffer_size);  // unmodified.
+}
diff --git a/testing/resources/named_dests.in b/testing/resources/named_dests.in
new file mode 100644 (file)
index 0000000..6655be9
--- /dev/null
@@ -0,0 +1,69 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Pages
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Catalog
+  /Pages 1 0 R
+  /Names <<
+    /Dests 10 0 R
+  >>
+  /Dests 14 0 R
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 1 0 R
+  /Resources <<
+    /ProcSets [/PDF /Text /ImageB /ImageC /ImageI]
+  >>
+  /MediaBox [0 0 612 792]
+  /Contents 5 0 R
+>>
+endobj
+{{object 5 0}} <<
+  /Length 18
+>> stream
+1 0 0 -1 29 763 cm
+endstream
+endobj
+% Root of Dests NameTree
+{{object 10 0}} <<
+  /Kids [
+    11 0 R
+    12 0 R
+  ]
+>>
+endobj
+% Left child for Dests NameTree
+{{object 11 0}} <<
+  /Names [
+    (First) [0 /XYZ 0 0 1]
+    (Next) <</D [4 /Fit]>>
+  ]
+>>
+endobj
+% Right child for Dests NameTree
+{{object 12 0}} <<
+  /Names [
+    (WrongKey)  <</Fail [10 /FitH]>>
+    (WrongType) /NameNotAllowedHere
+  ]
+>>
+endobj
+% Old-style top-level Dests dictionary.
+{{object 14 0}} <<
+  /FirstAlternate [10 /XYZ 200 400 800]
+  /LastAlternate  <</D [14 /XYZ 0 0 -200]>>
+>>
+endobj
+{{xref}}
+trailer <<
+  /Size 6
+  /Root 2 0 R
+>>
+{{startxref}}
+%%EOF
diff --git a/testing/resources/named_dests.pdf b/testing/resources/named_dests.pdf
new file mode 100644 (file)
index 0000000..1f650e7
--- /dev/null
@@ -0,0 +1,87 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+  /Type /Pages
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+2 0 obj <<
+  /Type /Catalog
+  /Pages 1 0 R
+  /Names <<
+    /Dests 10 0 R
+  >>
+  /Dests 14 0 R
+>>
+endobj
+3 0 obj <<
+  /Type /Page
+  /Parent 1 0 R
+  /Resources <<
+    /ProcSets [/PDF /Text /ImageB /ImageC /ImageI]
+  >>
+  /MediaBox [0 0 612 792]
+  /Contents 5 0 R
+>>
+endobj
+5 0 obj <<
+  /Length 18
+>> stream
+1 0 0 -1 29 763 cm
+endstream
+endobj
+% Root of Dests NameTree
+10 0 obj <<
+  /Kids [
+    11 0 R
+    12 0 R
+  ]
+>>
+endobj
+% Left child for Dests NameTree
+11 0 obj <<
+  /Names [
+    (First) [0 /XYZ 0 0 1]
+    (Next) <</D [4 /Fit]>>
+  ]
+>>
+endobj
+% Right child for Dests NameTree
+12 0 obj <<
+  /Names [
+    (WrongKey)  <</Fail [10 /FitH]>>
+    (WrongType) /NameNotAllowedHere
+  ]
+>>
+endobj
+% Old-style top-level Dests dictionary.
+14 0 obj <<
+  /FirstAlternate [10 /XYZ 200 400 800]
+  /LastAlternate  <</D [14 /XYZ 0 0 -200]>>
+>>
+endobj
+xref
+0 15
+0000000000 65536 f
+0000000015 00000 n
+0000000078 00000 n
+0000000182 00000 n
+0000000000 65536 f
+0000000349 00000 n
+0000000000 65536 f
+0000000000 65536 f
+0000000000 65536 f
+0000000000 65536 f
+0000000444 00000 n
+0000000534 00000 n
+0000000658 00000 n
+0000000000 65536 f
+0000000808 00000 n
+trailer <<
+  /Size 6
+  /Root 2 0 R
+>>
+startxref
+914
+%%EOF
diff --git a/testing/tools/fixup_pdf_template.py b/testing/tools/fixup_pdf_template.py
new file mode 100755 (executable)
index 0000000..873caee
--- /dev/null
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+# Copyright 2014 The PDFium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Expands a hand-written PDF testcase (template) into a valid PDF file.
+
+There are several places in a PDF file where byte-offsets are required. This
+script replaces {{name}}-style variables in the input with calculated results
+
+  {{header}}     - expands to the header comment required for PDF files.
+  {{xref}}       - expands to a generated xref table, noting the offset.
+  {{startxref}   - expands to a startxref directive followed by correct offset.
+  {{object x y}} - expands to |x y obj| declaration, noting the offset."""
+
+import optparse
+import os
+import re
+import sys
+
+class TemplateProcessor:
+  HEADER_TOKEN =  '{{header}}'
+  HEADER_REPLACEMENT = '%PDF-1.7\n%\xa0\xf2\xa4\xf4'
+
+  XREF_TOKEN = '{{xref}}'
+  XREF_REPLACEMENT = 'xref\n%d %d\n'
+  XREF_REPLACEMENT_N = '%010d %05d n\n'
+  XREF_REPLACEMENT_F = '0000000000 65536 f\n'
+
+  STARTXREF_TOKEN= '{{startxref}}'
+  STARTXREF_REPLACEMENT = 'startxref\n%d'
+
+  OBJECT_PATTERN = r'\{\{object\s+(\d+)\s+(\d+)\}\}'
+  OBJECT_REPLACEMENT = r'\1 \2 obj'
+
+  def __init__(self):
+    self.offset = 0
+    self.xref_offset = 0
+    self.max_object_number = 0
+    self.objects = { }
+
+  def insert_xref_entry(self, object_number, generation_number):
+    self.objects[object_number] = (self.offset, generation_number)
+    self.max_object_number = max(self.max_object_number, object_number)
+
+  def generate_xref_table(self):
+    result = self.XREF_REPLACEMENT % (0, self.max_object_number + 1)
+    for i in range(0, self.max_object_number + 1):
+      if i in self.objects:
+        result += self.XREF_REPLACEMENT_N % self.objects[i]
+      else:
+        result += self.XREF_REPLACEMENT_F
+    return result
+
+  def process_line(self, line):
+    if self.HEADER_TOKEN in line:
+      line = line.replace(self.HEADER_TOKEN, self.HEADER_REPLACEMENT)
+    if self.XREF_TOKEN in line:
+      self.xref_offset = self.offset
+      line = self.generate_xref_table()
+    if self.STARTXREF_TOKEN in line:
+      replacement = self.STARTXREF_REPLACEMENT % self.xref_offset
+      line = line.replace(self.STARTXREF_TOKEN, replacement)
+    match = re.match(self.OBJECT_PATTERN, line)
+    if match:
+      self.insert_xref_entry(int(match.group(1)), int(match.group(2)))
+      line = re.sub(self.OBJECT_PATTERN, self.OBJECT_REPLACEMENT, line)
+    self.offset += len(line)
+    return line
+
+def expand_file(input_filename):
+  (input_root, extension) = os.path.splitext(input_filename)
+  output_filename = input_root + '.pdf'
+  processor = TemplateProcessor()
+  try:
+    with open(input_filename, 'r') as infile:
+      with open(output_filename, 'w') as outfile:
+        for line in infile:
+           outfile.write(processor.process_line(line))
+  except IOError:
+    print >> sys.stderr, 'failed to process %s' % input_filename
+
+def main():
+  for arg in sys.argv[1:]:
+    expand_file(arg)
+  return 0
+
+if __name__ == '__main__':
+  sys.exit(main())