exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

mhd_typst.c (22063B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file mhd_typst.c
     18  * @brief MHD utility functions for PDF generation
     19  * @author Christian Grothoff
     20  *
     21  *
     22  */
     23 #include "taler/platform.h"
     24 #include "taler/taler_util.h"
     25 #include "taler/taler_mhd_lib.h"
     26 #include <microhttpd.h>
     27 
     28 
     29 /**
     30  * Information about a specific typst invocation.
     31  */
     32 struct TypstStage
     33 {
     34   /**
     35    * Name of the FIFO for the typst output.
     36    */
     37   char *filename;
     38 
     39   /**
     40    * Typst context we are part of.
     41    */
     42   struct TALER_MHD_TypstContext *tc;
     43 
     44   /**
     45    * Handle to the typst process.
     46    */
     47   struct GNUNET_OS_Process *proc;
     48 
     49   /**
     50    * Handle to be notified about stage completion.
     51    */
     52   struct GNUNET_ChildWaitHandle *cwh;
     53 
     54 };
     55 
     56 
     57 struct TALER_MHD_TypstContext
     58 {
     59 
     60   /**
     61    * Directory where we create temporary files (or FIFOs) for the IPC.
     62    */
     63   char *tmpdir;
     64 
     65   /**
     66    * Array of stages producing PDFs to be combined.
     67    */
     68   struct TypstStage *stages;
     69 
     70   /**
     71    * Handle for pdftk combining the various PDFs.
     72    */
     73   struct GNUNET_OS_Process *proc;
     74 
     75   /**
     76    * Handle to wait for @e proc to complete.
     77    */
     78   struct GNUNET_ChildWaitHandle *cwh;
     79 
     80   /**
     81    * Callback to call on the final result.
     82    */
     83   TALER_MHD_TypstResultCallback cb;
     84 
     85   /**
     86    * Closure for @e cb
     87    */
     88   void *cb_cls;
     89 
     90   /**
     91    * Task for async work.
     92    */
     93   struct GNUNET_SCHEDULER_Task *t;
     94 
     95   /**
     96    * Name of the final file created by pdftk.
     97    */
     98   char *output_file;
     99 
    100   /**
    101    * Context for fail_async_cb().
    102    */
    103   char *async_hint;
    104 
    105   /**
    106    * Context for fail_async_cb().
    107    */
    108   enum TALER_ErrorCode async_ec;
    109 
    110   /**
    111    * Length of the @e stages array.
    112    */
    113   unsigned int num_stages;
    114 
    115   /**
    116    * Number of still active stages.
    117    */
    118   unsigned int active_stages;
    119 
    120   /**
    121    * Should the directory be removed when done?
    122    */
    123   bool remove_on_exit;
    124 };
    125 
    126 
    127 void
    128 TALER_MHD_typst_cancel (struct TALER_MHD_TypstContext *tc)
    129 {
    130   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    131               "Cleaning up TypstContext\n");
    132   if (NULL != tc->t)
    133   {
    134     GNUNET_SCHEDULER_cancel (tc->t);
    135     tc->t = NULL;
    136   }
    137   for (unsigned int i = 0; i<tc->num_stages; i++)
    138   {
    139     struct TypstStage *stage = &tc->stages[i];
    140 
    141     if (NULL != stage->cwh)
    142     {
    143       GNUNET_wait_child_cancel (stage->cwh);
    144       stage->cwh = NULL;
    145     }
    146     if (NULL != stage->proc)
    147     {
    148       GNUNET_break (0 ==
    149                     GNUNET_OS_process_kill (stage->proc,
    150                                             SIGKILL));
    151       GNUNET_OS_process_destroy (stage->proc);
    152       stage->proc = NULL;
    153     }
    154     GNUNET_free (stage->filename);
    155   }
    156   GNUNET_free (tc->stages);
    157   if (NULL != tc->cwh)
    158   {
    159     GNUNET_wait_child_cancel (tc->cwh);
    160     tc->cwh = NULL;
    161   }
    162   if (NULL != tc->proc)
    163   {
    164     GNUNET_break (0 ==
    165                   GNUNET_OS_process_kill (tc->proc,
    166                                           SIGKILL));
    167     GNUNET_OS_process_destroy (tc->proc);
    168   }
    169   GNUNET_free (tc->output_file);
    170   if (NULL != tc->tmpdir)
    171   {
    172     if (tc->remove_on_exit)
    173       GNUNET_DISK_directory_remove (tc->tmpdir);
    174     GNUNET_free (tc->tmpdir);
    175   }
    176   GNUNET_free (tc);
    177 }
    178 
    179 
    180 /**
    181  * Create file in @a tmpdir with one of the PDF inputs.
    182  *
    183  * @param[out] stage initialized stage data
    184  * @param tmpdir where to place temporary files
    185  * @param data input JSON with PDF data
    186  * @return true on success
    187  */
    188 static bool
    189 inline_pdf_stage (struct TypstStage *stage,
    190                   const char *tmpdir,
    191                   const json_t *data)
    192 {
    193   const char *str = json_string_value (data);
    194   char *fn;
    195   size_t n;
    196   void *b;
    197   int fd;
    198 
    199   if (NULL == str)
    200   {
    201     GNUNET_break (0);
    202     return false;
    203   }
    204   b = NULL;
    205   n = GNUNET_STRINGS_base64_decode (str,
    206                                     strlen (str),
    207                                     &b);
    208   if (NULL == b)
    209   {
    210     GNUNET_break (0);
    211     return false;
    212   }
    213   GNUNET_asprintf (&fn,
    214                    "%s/external-",
    215                    tmpdir);
    216   stage->filename = GNUNET_DISK_mktemp (fn);
    217   if (NULL == stage->filename)
    218   {
    219     GNUNET_break (0);
    220     GNUNET_free (b);
    221     GNUNET_free (fn);
    222     return false;
    223   }
    224   GNUNET_free (fn);
    225   fd = open (stage->filename,
    226              O_WRONLY | O_TRUNC,
    227              S_IRUSR | S_IWUSR);
    228   if (-1 == fd)
    229   {
    230     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    231                               "open",
    232                               stage->filename);
    233     GNUNET_free (b);
    234     GNUNET_free (stage->filename);
    235     return false;
    236   }
    237 
    238   {
    239     size_t off = 0;
    240 
    241     while (off < n)
    242     {
    243       ssize_t r;
    244 
    245       r = write (fd,
    246                  b + off,
    247                  n - off);
    248       if (-1 == r)
    249       {
    250         GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    251                                   "write",
    252                                   stage->filename);
    253         GNUNET_break (0 == close (fd));
    254         GNUNET_free (b);
    255         GNUNET_free (stage->filename);
    256         return false;
    257       }
    258       off += r;
    259     }
    260   }
    261   GNUNET_break (0 == close (fd));
    262   return true;
    263 }
    264 
    265 
    266 /**
    267  * Generate a response for @a tc indicating an error of type @a ec.
    268  *
    269  * @param[in,out] tc context to fail
    270  * @param ec error code to return
    271  * @param hint hint text to return
    272  */
    273 static void
    274 typst_context_fail (struct TALER_MHD_TypstContext *tc,
    275                     enum TALER_ErrorCode ec,
    276                     const char *hint)
    277 {
    278   struct TALER_MHD_TypstResponse resp = {
    279     .ec = ec,
    280     .details.hint = hint
    281   };
    282 
    283   if (NULL != tc->cb)
    284   {
    285     tc->cb (tc->cb_cls,
    286             &resp);
    287     tc->cb = NULL;
    288   }
    289 }
    290 
    291 
    292 /**
    293  * Helper task for typst_context_fail_async().
    294  *
    295  * @param cls a `struct TALER_MHD_TypstContext`
    296  */
    297 static void
    298 fail_async_cb (void *cls)
    299 {
    300   struct TALER_MHD_TypstContext *tc = cls;
    301 
    302   tc->t = NULL;
    303   typst_context_fail (tc,
    304                       tc->async_ec,
    305                       tc->async_hint);
    306   GNUNET_free (tc->async_hint);
    307   TALER_MHD_typst_cancel (tc);
    308 }
    309 
    310 
    311 /**
    312  * Generate a response for @a tc indicating an error of type @a ec.
    313  *
    314  * @param[in,out] tc context to fail
    315  * @param ec error code to return
    316  * @param hint hint text to return
    317  */
    318 static void
    319 typst_context_fail_async (struct TALER_MHD_TypstContext *tc,
    320                           enum TALER_ErrorCode ec,
    321                           const char *hint)
    322 {
    323   tc->async_ec = ec;
    324   tc->async_hint = (NULL == hint) ? NULL : GNUNET_strdup (hint);
    325   tc->t = GNUNET_SCHEDULER_add_now (&fail_async_cb,
    326                                     tc);
    327 }
    328 
    329 
    330 /**
    331  * Called when the pdftk helper exited.
    332  *
    333  * @param cls our `struct TALER_MHD_TypstContext *`
    334  * @param type type of the process
    335  * @param exit_code status code of the process
    336  */
    337 static void
    338 pdftk_done_cb (void *cls,
    339                enum GNUNET_OS_ProcessStatusType type,
    340                long unsigned int exit_code)
    341 {
    342   struct TALER_MHD_TypstContext *tc = cls;
    343 
    344   tc->cwh = NULL;
    345   GNUNET_OS_process_destroy (tc->proc);
    346   tc->proc = NULL;
    347   switch (type)
    348   {
    349   case GNUNET_OS_PROCESS_UNKNOWN:
    350     GNUNET_assert (0);
    351     return;
    352   case GNUNET_OS_PROCESS_RUNNING:
    353     /* we should not get this notification */
    354     GNUNET_break (0);
    355     return;
    356   case GNUNET_OS_PROCESS_STOPPED:
    357     /* Someone is SIGSTOPing our helper!? */
    358     GNUNET_break (0);
    359     return;
    360   case GNUNET_OS_PROCESS_EXITED:
    361     if (0 != exit_code)
    362     {
    363       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    364                   "pdftk exited with status %d\n",
    365                   (int) exit_code);
    366       typst_context_fail (tc,
    367                           TALER_EC_EXCHANGE_GENERIC_PDFTK_FAILURE,
    368                           "pdftk failed");
    369     }
    370     else
    371     {
    372       struct TALER_MHD_TypstResponse resp = {
    373         .ec = TALER_EC_NONE,
    374         .details.filename = tc->output_file,
    375       };
    376 
    377       GNUNET_assert (NULL != tc->cb);
    378       tc->cb (tc->cb_cls,
    379               &resp);
    380       tc->cb = NULL;
    381     }
    382     break;
    383   case GNUNET_OS_PROCESS_SIGNALED:
    384     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    385                 "pdftk died with signal %d\n",
    386                 (int) exit_code);
    387     typst_context_fail (tc,
    388                         TALER_EC_EXCHANGE_GENERIC_PDFTK_CRASH,
    389                         "pdftk killed by signal");
    390     break;
    391   }
    392   TALER_MHD_typst_cancel (tc);
    393 }
    394 
    395 
    396 /**
    397  * Function called once all of the individual stages are done.
    398  * Triggers the pdftk run for @a tc.
    399  *
    400  * @param[in,out] cls a `struct TALER_MHD_TypstContext *` context to run pdftk for
    401  */
    402 static void
    403 complete_response (void *cls)
    404 {
    405   struct TALER_MHD_TypstContext *tc = cls;
    406   const char *argv[tc->num_stages + 5];
    407 
    408   tc->t = NULL;
    409   argv[0] = "pdftk";
    410   for (unsigned int i = 0; i<tc->num_stages; i++)
    411     argv[i + 1] = tc->stages[i].filename;
    412   argv[tc->num_stages + 1] = "cat";
    413   argv[tc->num_stages + 2] = "output";
    414   argv[tc->num_stages + 3] = tc->output_file;
    415   argv[tc->num_stages + 4] = NULL;
    416   tc->proc = GNUNET_OS_start_process_vap (
    417     GNUNET_OS_INHERIT_STD_ERR,
    418     NULL,
    419     NULL,
    420     NULL,
    421     argv[0],
    422     (char **) argv);
    423   if (NULL == tc->proc)
    424   {
    425     GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
    426                          "fork");
    427     TALER_MHD_typst_cancel (tc);
    428     return;
    429   }
    430   tc->cwh = GNUNET_wait_child (tc->proc,
    431                                &pdftk_done_cb,
    432                                tc);
    433   GNUNET_assert (NULL != tc->cwh);
    434 }
    435 
    436 
    437 /**
    438  * Cancel typst. Wrapper task to do so asynchronously.
    439  *
    440  * @param[in] cls a `struct TALER_MHD_TypstContext`
    441  */
    442 static void
    443 cancel_async (void *cls)
    444 {
    445   struct TALER_MHD_TypstContext *tc = cls;
    446 
    447   tc->t = NULL;
    448   TALER_MHD_typst_cancel (tc);
    449 }
    450 
    451 
    452 /**
    453  * Called when a typst helper exited.
    454  *
    455  * @param cls our `struct TypstStage *`
    456  * @param type type of the process
    457  * @param exit_code status code of the process
    458  */
    459 static void
    460 typst_done_cb (void *cls,
    461                enum GNUNET_OS_ProcessStatusType type,
    462                long unsigned int exit_code)
    463 {
    464   struct TypstStage *stage = cls;
    465   struct TALER_MHD_TypstContext *tc = stage->tc;
    466 
    467   stage->cwh = NULL;
    468   GNUNET_OS_process_destroy (stage->proc);
    469   stage->proc = NULL;
    470   switch (type)
    471   {
    472   case GNUNET_OS_PROCESS_UNKNOWN:
    473     GNUNET_assert (0);
    474     return;
    475   case GNUNET_OS_PROCESS_RUNNING:
    476     /* we should not get this notification */
    477     GNUNET_break (0);
    478     return;
    479   case GNUNET_OS_PROCESS_STOPPED:
    480     /* Someone is SIGSTOPing our helper!? */
    481     GNUNET_break (0);
    482     return;
    483   case GNUNET_OS_PROCESS_EXITED:
    484     if (0 != exit_code)
    485     {
    486       char err[128];
    487 
    488       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    489                   "typst exited with status %d\n",
    490                   (int) exit_code);
    491       GNUNET_snprintf (err,
    492                        sizeof (err),
    493                        "Typst exited with status %d",
    494                        (int) exit_code);
    495       typst_context_fail (tc,
    496                           TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE,
    497                           err);
    498       GNUNET_assert (NULL == tc->t);
    499       tc->t = GNUNET_SCHEDULER_add_now (&cancel_async,
    500                                         tc);
    501       return;
    502     }
    503     break;
    504   case GNUNET_OS_PROCESS_SIGNALED:
    505     {
    506       char err[128];
    507 
    508       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    509                   "typst died with signal %d\n",
    510                   (int) exit_code);
    511       GNUNET_snprintf (err,
    512                        sizeof (err),
    513                        "Typst died with signal %d",
    514                        (int) exit_code);
    515       typst_context_fail (tc,
    516                           TALER_EC_EXCHANGE_GENERIC_TYPST_CRASH,
    517                           err);
    518       GNUNET_assert (NULL == tc->t);
    519       tc->t = GNUNET_SCHEDULER_add_now (&cancel_async,
    520                                         tc);
    521       return;
    522     }
    523     break;
    524   }
    525   tc->active_stages--;
    526   if (NULL != stage->proc)
    527   {
    528     GNUNET_OS_process_destroy (stage->proc);
    529     stage->proc = NULL;
    530   }
    531   if (0 != tc->active_stages)
    532     return;
    533   GNUNET_assert (NULL == tc->t);
    534   tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
    535                                     tc);
    536 }
    537 
    538 
    539 /**
    540  * Setup typst stage to produce one of the PDF inputs.
    541  *
    542  * @param[out] stage initialized stage data
    543  * @param i index of the stage
    544  * @param tmpdir where to place temporary files
    545  * @param template_path where to find templates
    546  * @param doc input document specification
    547  * @return true on success
    548  */
    549 static bool
    550 setup_stage (struct TypstStage *stage,
    551              unsigned int i,
    552              const char *tmpdir,
    553              const char *template_path,
    554              const struct TALER_MHD_TypstDocument *doc)
    555 {
    556   char *input;
    557   bool is_dot_typ;
    558 
    559   if (NULL == doc->form_name)
    560   {
    561     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    562                 "Stage %u: Dumping inlined PDF attachment\n",
    563                 i);
    564     return inline_pdf_stage (stage,
    565                              tmpdir,
    566                              doc->data);
    567   }
    568 
    569   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    570               "Stage %u: Handling form %s\n",
    571               i,
    572               doc->form_name);
    573 
    574   /* Setup inputs */
    575   {
    576     char *dirname;
    577 
    578     GNUNET_asprintf (&dirname,
    579                      "%s/%u/",
    580                      tmpdir,
    581                      i);
    582     if (GNUNET_OK !=
    583         GNUNET_DISK_directory_create (dirname))
    584     {
    585       GNUNET_free (dirname);
    586       return false;
    587     }
    588     GNUNET_free (dirname);
    589   }
    590 
    591   /* Setup data input */
    592   {
    593     char *jfn;
    594 
    595     GNUNET_asprintf (&jfn,
    596                      "%s/%u/input.json",
    597                      tmpdir,
    598                      i);
    599     if (0 !=
    600         json_dump_file (doc->data,
    601                         jfn,
    602                         JSON_INDENT (2)))
    603     {
    604       GNUNET_break (0);
    605       GNUNET_free (jfn);
    606       return false;
    607     }
    608     GNUNET_free (jfn);
    609   }
    610 
    611   /* setup output file name */
    612   GNUNET_asprintf (&stage->filename,
    613                    "%s/%u/input.pdf",
    614                    tmpdir,
    615                    i);
    616 
    617   /* setup main input Typst file */
    618   {
    619     char *intyp;
    620     size_t slen = strlen (doc->form_name);
    621 
    622     is_dot_typ = ( (slen > 4) &&
    623                    (0 == memcmp (&doc->form_name[slen - 4],
    624                                  ".typ",
    625                                  4)) );
    626     /* We do not append the ":$VERSION" if a filename ending with ".typ"
    627        is given. Otherwise we append the version, or ":0.0.0" if no
    628        explicit version is given. */
    629     GNUNET_asprintf (&intyp,
    630                      "#import \"%s/%s%s%s\": form\n"
    631                      "#form(json(\"input.json\"))\n",
    632                      template_path,
    633                      doc->form_name,
    634                      is_dot_typ ? "" : ":",
    635                      is_dot_typ
    636                      ? ""
    637                      : ( (NULL == doc->form_version)
    638                          ? "0.0.0"
    639                          : doc->form_version));
    640     GNUNET_asprintf (&input,
    641                      "%s/%u/input.typ",
    642                      tmpdir,
    643                      i);
    644     if (GNUNET_OK !=
    645         GNUNET_DISK_fn_write (input,
    646                               intyp,
    647                               strlen (intyp),
    648                               GNUNET_DISK_PERM_USER_READ))
    649     {
    650       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    651                                 "write",
    652                                 input);
    653       GNUNET_free (input);
    654       GNUNET_free (intyp);
    655       return false;
    656     }
    657     GNUNET_free (intyp);
    658   }
    659 
    660   /* now setup typst invocation */
    661   {
    662     const char *argv[6];
    663 
    664     if (is_dot_typ)
    665     {
    666       /* This deliberately breaks the typst sandbox. Why? Because Typst sucks
    667          and does not support multiple roots, but here we have dynamic data in
    668          /tmp and a style file outside of /tmp (and copying is also not
    669          practical as we don't know what all to copy). Typst should really
    670          support multiple roots. Anyway, in production this path should not
    671          happen, because there we use Typst packages. */
    672       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    673                   "Bypassing Typst sandbox. You should use Typst packages instead of `%s'.\n",
    674                   doc->form_name);
    675       argv[0] = "typst";
    676       argv[1] = "compile";
    677       argv[2] = "--root";
    678       argv[3] = "/";
    679       argv[4] = input;
    680       argv[5] = NULL;
    681     }
    682     else
    683     {
    684       argv[0] = "typst";
    685       argv[1] = "compile";
    686       argv[2] = input;
    687       argv[3] = NULL;
    688     }
    689     stage->proc = GNUNET_OS_start_process_vap (
    690       GNUNET_OS_INHERIT_STD_ERR,
    691       NULL,
    692       NULL,
    693       NULL,
    694       "typst",
    695       (char **) argv);
    696     if (NULL == stage->proc)
    697     {
    698       GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
    699                            "fork");
    700       GNUNET_free (input);
    701       return false;
    702     }
    703     GNUNET_free (input);
    704     stage->tc->active_stages++;
    705     stage->cwh = GNUNET_wait_child (stage->proc,
    706                                     &typst_done_cb,
    707                                     stage);
    708     GNUNET_assert (NULL != stage->cwh);
    709   }
    710   return true;
    711 }
    712 
    713 
    714 struct TALER_MHD_TypstContext *
    715 TALER_MHD_typst (
    716   const struct GNUNET_CONFIGURATION_Handle *cfg,
    717   bool remove_on_exit,
    718   const char *cfg_section_name,
    719   unsigned int num_documents,
    720   const struct TALER_MHD_TypstDocument docs[static num_documents],
    721   TALER_MHD_TypstResultCallback cb,
    722   void *cb_cls)
    723 {
    724   static enum GNUNET_GenericReturnValue once = GNUNET_NO;
    725   struct TALER_MHD_TypstContext *tc;
    726 
    727   switch (once)
    728   {
    729   case GNUNET_OK:
    730     break;
    731   case GNUNET_NO:
    732     if (GNUNET_SYSERR ==
    733         GNUNET_OS_check_helper_binary ("typst",
    734                                        false,
    735                                        NULL))
    736     {
    737       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    738                   "`typst' command not found\n");
    739       once = GNUNET_SYSERR;
    740       return NULL;
    741     }
    742     if (GNUNET_SYSERR ==
    743         GNUNET_OS_check_helper_binary ("pdftk",
    744                                        false,
    745                                        NULL))
    746     {
    747       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    748                   "`pdftk' command not found\n");
    749       once = GNUNET_SYSERR;
    750       return NULL;
    751     }
    752     once = GNUNET_OK;
    753     break;
    754   case GNUNET_SYSERR:
    755     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    756                 "PDF generation initialization failed before, not even trying again\n");
    757     return NULL;
    758   }
    759   tc = GNUNET_new (struct TALER_MHD_TypstContext);
    760   tc->tmpdir = GNUNET_strdup ("/tmp/taler-typst-XXXXXX");
    761   tc->remove_on_exit = remove_on_exit;
    762   tc->cb = cb;
    763   tc->cb_cls = cb_cls;
    764   if (NULL == mkdtemp (tc->tmpdir))
    765   {
    766     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    767                               "mkdtemp",
    768                               tc->tmpdir);
    769     GNUNET_free (tc->tmpdir);
    770     TALER_MHD_typst_cancel (tc);
    771     return NULL;
    772   }
    773   GNUNET_asprintf (&tc->output_file,
    774                    "%s/final.pdf",
    775                    tc->tmpdir);
    776 
    777   /* setup typst stages */
    778   {
    779     char *template_path;
    780 
    781     if (GNUNET_OK !=
    782         GNUNET_CONFIGURATION_get_value_string (cfg,
    783                                                cfg_section_name,
    784                                                "TYPST_TEMPLATES",
    785                                                &template_path))
    786     {
    787       GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    788                                  cfg_section_name,
    789                                  "TYPST_TEMPLATES");
    790       TALER_MHD_typst_cancel (tc);
    791       return NULL;
    792     }
    793     if ('@' != template_path[0])
    794       template_path = GNUNET_CONFIGURATION_expand_dollar (cfg,
    795                                                           template_path);
    796     tc->stages = GNUNET_new_array (num_documents,
    797                                    struct TypstStage);
    798     tc->num_stages = num_documents;
    799     for (unsigned int i = 0; i<num_documents; i++)
    800     {
    801       tc->stages[i].tc = tc;
    802       if (! setup_stage (&tc->stages[i],
    803                          i,
    804                          tc->tmpdir,
    805                          template_path,
    806                          &docs[i]))
    807       {
    808         char err[128];
    809 
    810         GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    811                     "Typst setup failed on stage %u\n",
    812                     i);
    813         GNUNET_snprintf (err,
    814                          sizeof (err),
    815                          "Typst setup failed on stage %u",
    816                          i);
    817         typst_context_fail_async (tc,
    818                                   TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE,
    819                                   err);
    820         return tc;
    821       }
    822     }
    823     GNUNET_free (template_path);
    824   }
    825   if (0 == tc->active_stages)
    826   {
    827     tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
    828                                       tc);
    829   }
    830   return tc;
    831 }
    832 
    833 
    834 struct MHD_Response *
    835 TALER_MHD_response_from_pdf_file (const char *filename)
    836 {
    837   struct MHD_Response *resp;
    838   struct stat s;
    839   int fd;
    840 
    841   fd = open (filename,
    842              O_RDONLY);
    843   if (-1 == fd)
    844   {
    845     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    846                               "open",
    847                               filename);
    848     return NULL;
    849   }
    850   if (0 !=
    851       fstat (fd,
    852              &s))
    853   {
    854     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    855                               "fstat",
    856                               filename);
    857     GNUNET_assert (0 == close (fd));
    858     return NULL;
    859   }
    860   resp = MHD_create_response_from_fd (s.st_size,
    861                                       fd);
    862   TALER_MHD_add_global_headers (resp,
    863                                 false);
    864   GNUNET_break (MHD_YES ==
    865                 MHD_add_response_header (resp,
    866                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    867                                          "application/pdf"));
    868   return resp;
    869 }