add file annot api

Added experimental public API for getting
and creating file attachment annotations.

Bug: pdfium:2125
Change-Id: Idc405e88eed1c8efaa7d690c11251ad8f4ede64d
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/116490
Reviewed-by: Thomas Sepez <tsepez@google.com>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Thomas Sepez <tsepez@google.com>
diff --git a/AUTHORS b/AUTHORS
index 0689774..6e4b6e4 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -22,6 +22,7 @@
 Felix Kauselmann <licorn@gmail.com>
 GiWan Go <gogil@stealien.com>
 Huy Ngo <huyna89@gmail.com>
+Ivan Odulo <ivanodulo@gmail.com>
 Jiang Jiang <jiangj@opera.com>
 Justin Pierce <brkfstmnchr@gmail.com>
 Ke Liu <stackexploit@gmail.com>
diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp
index c4b912b..573a072 100644
--- a/fpdfsdk/fpdf_annot.cpp
+++ b/fpdfsdk/fpdf_annot.cpp
@@ -341,6 +341,7 @@
   // The supported subtypes must also be communicated in the user doc.
   switch (subtype) {
     case FPDF_ANNOT_CIRCLE:
+    case FPDF_ANNOT_FILEATTACHMENT:
     case FPDF_ANNOT_FREETEXT:
     case FPDF_ANNOT_HIGHLIGHT:
     case FPDF_ANNOT_INK:
@@ -1473,3 +1474,51 @@
   action->SetNewFor<CPDF_String>("URI", uri, /*bHex=*/false);
   return true;
 }
+
+FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV
+FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot) {
+  if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FILEATTACHMENT) {
+    return nullptr;
+  }
+
+  RetainPtr<CPDF_Dictionary> annot_dict =
+      GetMutableAnnotDictFromFPDFAnnotation(annot);
+  if (!annot_dict) {
+    return nullptr;
+  }
+
+  return FPDFAttachmentFromCPDFObject(
+      annot_dict->GetMutableDirectObjectFor("FS"));
+}
+
+FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV
+FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, FPDF_WIDESTRING name) {
+  if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FILEATTACHMENT) {
+    return nullptr;
+  }
+
+  CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot);
+  if (!context) {
+    return nullptr;
+  }
+
+  RetainPtr<CPDF_Dictionary> annot_dict = context->GetMutableAnnotDict();
+  if (!annot_dict) {
+    return nullptr;
+  }
+
+  WideString ws_name = WideStringFromFPDFWideString(name);
+  if (ws_name.IsEmpty()) {
+    return nullptr;
+  }
+
+  CPDF_Document* doc = context->GetPage()->GetDocument();
+  auto fs_obj = doc->NewIndirect<CPDF_Dictionary>();
+
+  fs_obj->SetNewFor<CPDF_Name>("Type", "Filespec");
+  fs_obj->SetNewFor<CPDF_String>("UF", ws_name.AsStringView());
+  fs_obj->SetNewFor<CPDF_String>("F", ws_name.AsStringView());
+
+  annot_dict->SetNewFor<CPDF_Reference>("FS", doc, fs_obj->GetObjNum());
+  return FPDFAttachmentFromCPDFObject(fs_obj);
+}
diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp
index ca3efa7..17fde6a 100644
--- a/fpdfsdk/fpdf_annot_embeddertest.cpp
+++ b/fpdfsdk/fpdf_annot_embeddertest.cpp
@@ -21,6 +21,7 @@
 #include "core/fxge/cfx_defaultrenderdevice.h"
 #include "fpdfsdk/cpdfsdk_helpers.h"
 #include "public/cpp/fpdf_scopers.h"
+#include "public/fpdf_attachment.h"
 #include "public/fpdf_edit.h"
 #include "public/fpdf_formfill.h"
 #include "public/fpdfview.h"
@@ -3830,3 +3831,116 @@
   CloseSavedPage(page);
   CloseSavedDocument();
 }
+
+TEST_F(FPDFAnnotEmbedderTest, GetAndAddFileAttachmentAnnotation) {
+  ASSERT_TRUE(OpenDocument("annotation_fileattachment.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+  EXPECT_EQ(1, FPDFPage_GetAnnotCount(page));
+
+  {
+    ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 0));
+    ASSERT_TRUE(annot);
+    EXPECT_EQ(FPDF_ANNOT_FILEATTACHMENT, FPDFAnnot_GetSubtype(annot.get()));
+
+    FPDF_ATTACHMENT attachment = FPDFAnnot_GetFileAttachment(annot.get());
+    ASSERT_TRUE(attachment);
+
+    // Check that the name of the attachment is correct.
+    unsigned long length_bytes = FPDFAttachment_GetName(attachment, nullptr, 0);
+    ASSERT_EQ(18u, length_bytes);
+    std::vector<FPDF_WCHAR> buf = GetFPDFWideStringBuffer(length_bytes);
+    EXPECT_EQ(18u,
+              FPDFAttachment_GetName(attachment, buf.data(), length_bytes));
+    EXPECT_EQ(L"test.txt", GetPlatformWString(buf.data()));
+
+    // Check that the content of the attachment is correct.
+    ASSERT_TRUE(FPDFAttachment_GetFile(attachment, nullptr, 0, &length_bytes));
+    std::vector<uint8_t> content_buf(length_bytes);
+    unsigned long actual_length_bytes;
+    ASSERT_TRUE(FPDFAttachment_GetFile(attachment, content_buf.data(),
+                                       length_bytes, &actual_length_bytes));
+    ASSERT_THAT(content_buf, testing::ElementsAre('t', 'e', 's', 't', ' ', 't',
+                                                  'e', 'x', 't'));
+  }
+
+  {
+    // Add a file attachment annotation to the page.
+    ScopedFPDFAnnotation annot(
+        FPDFPage_CreateAnnot(page, FPDF_ANNOT_FILEATTACHMENT));
+    ASSERT_TRUE(annot);
+
+    // Check that there is now 2 annotations on this page.
+    EXPECT_EQ(2, FPDFPage_GetAnnotCount(page));
+
+    ScopedFPDFWideString file_name = GetFPDFWideString(L"0.txt");
+    FPDF_ATTACHMENT attachment =
+        FPDFAnnot_AddFileAttachment(annot.get(), file_name.get());
+    ASSERT_TRUE(attachment);
+
+    // The filling of the FPDF_ATTACHMENT has been tested in
+    // fpdf_attachment_embeddertest.cpp
+  }
+
+  {
+    ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 1));
+    ASSERT_TRUE(annot);
+    EXPECT_EQ(FPDF_ANNOT_FILEATTACHMENT, FPDFAnnot_GetSubtype(annot.get()));
+
+    // Check that we can read newly created file spec
+    FPDF_ATTACHMENT attachment = FPDFAnnot_GetFileAttachment(annot.get());
+    ASSERT_TRUE(attachment);
+
+    // Verify the name of the new attachment.
+    unsigned long length_bytes = FPDFAttachment_GetName(attachment, nullptr, 0);
+    ASSERT_EQ(12u, length_bytes);
+    std::vector<FPDF_WCHAR> buf = GetFPDFWideStringBuffer(length_bytes);
+    EXPECT_EQ(12u,
+              FPDFAttachment_GetName(attachment, buf.data(), length_bytes));
+    EXPECT_EQ(L"0.txt", GetPlatformWString(buf.data()));
+  }
+
+  UnloadPage(page);
+}
+
+TEST_F(FPDFAnnotEmbedderTest, BadCasesFileAttachmentAnnotation) {
+  ASSERT_TRUE(OpenDocument("annotation_fileattachment.pdf"));
+  FPDF_PAGE page = LoadPage(0);
+  ASSERT_TRUE(page);
+  EXPECT_EQ(1, FPDFPage_GetAnnotCount(page));
+
+  {
+    ASSERT_FALSE(FPDFAnnot_GetFileAttachment(nullptr));
+
+    ScopedFPDFAnnotation text_annot(
+        FPDFPage_CreateAnnot(page, FPDF_ANNOT_TEXT));
+    ASSERT_TRUE(text_annot);
+    ASSERT_FALSE(FPDFAnnot_GetFileAttachment(text_annot.get()));
+
+    ScopedFPDFAnnotation newly_file_annot(
+        FPDFPage_CreateAnnot(page, FPDF_ANNOT_FILEATTACHMENT));
+    ASSERT_TRUE(newly_file_annot);
+    ASSERT_FALSE(FPDFAnnot_GetFileAttachment(newly_file_annot.get()));
+  }
+
+  {
+    ScopedFPDFWideString empty_name = GetFPDFWideString(L"");
+    ScopedFPDFWideString not_empty_name = GetFPDFWideString(L"0.txt");
+
+    ASSERT_FALSE(FPDFAnnot_AddFileAttachment(nullptr, nullptr));
+    ASSERT_FALSE(FPDFAnnot_AddFileAttachment(nullptr, empty_name.get()));
+    ASSERT_FALSE(FPDFAnnot_AddFileAttachment(nullptr, not_empty_name.get()));
+
+    ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 0));
+    ASSERT_TRUE(annot);
+
+    ASSERT_FALSE(FPDFAnnot_AddFileAttachment(annot.get(), nullptr));
+    ASSERT_FALSE(FPDFAnnot_AddFileAttachment(annot.get(), empty_name.get()));
+
+    FPDF_ATTACHMENT old_attachment = FPDFAnnot_GetFileAttachment(annot.get());
+    EXPECT_NE(old_attachment,
+              FPDFAnnot_AddFileAttachment(annot.get(), not_empty_name.get()));
+  }
+
+  UnloadPage(page);
+}
diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c
index 4f8bccf..f8c5483 100644
--- a/fpdfsdk/fpdf_view_c_api_test.c
+++ b/fpdfsdk/fpdf_view_c_api_test.c
@@ -40,6 +40,7 @@
 // Function to call from gtest harness to ensure linker resolution.
 int CheckPDFiumCApi() {
     // fpdf_annot.h
+    CHK(FPDFAnnot_AddFileAttachment);
     CHK(FPDFAnnot_AddInkStroke);
     CHK(FPDFAnnot_AppendAttachmentPoints);
     CHK(FPDFAnnot_AppendObject);
@@ -48,6 +49,7 @@
     CHK(FPDFAnnot_GetAttachmentPoints);
     CHK(FPDFAnnot_GetBorder);
     CHK(FPDFAnnot_GetColor);
+    CHK(FPDFAnnot_GetFileAttachment);
     CHK(FPDFAnnot_GetFlags);
     CHK(FPDFAnnot_GetFocusableSubtypes);
     CHK(FPDFAnnot_GetFocusableSubtypesCount);
diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h
index abcef6b..337da58 100644
--- a/public/fpdf_annot.h
+++ b/public/fpdf_annot.h
@@ -101,6 +101,7 @@
 // Check if an annotation subtype is currently supported for creation.
 // Currently supported subtypes:
 //    - circle
+//    - fileattachment
 //    - freetext
 //    - highlight
 //    - ink
@@ -968,6 +969,25 @@
 FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetURI(FPDF_ANNOTATION annot,
                                                      const char* uri);
 
+// Experimental API.
+// Get the attachment from |annot|.
+//
+//   annot - handle to a file annotation.
+//
+// Returns the handle to the attachment object, or NULL on failure.
+FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV
+FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot);
+
+// Experimental API.
+// Add an embedded file with |name| to |annot|.
+//
+//   annot    - handle to a file annotation.
+//   name     - name of the new attachment.
+//
+// Returns a handle to the new attachment object, or NULL on failure.
+FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV
+FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, FPDF_WIDESTRING name);
+
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus
diff --git a/testing/resources/annotation_fileattachment.pdf b/testing/resources/annotation_fileattachment.pdf
new file mode 100644
index 0000000..6d1f954
--- /dev/null
+++ b/testing/resources/annotation_fileattachment.pdf
Binary files differ