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 (22922B)


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