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


      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 "platform.h"  /* UNNECESSARY? */
     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     if (0 != unlink (stage->filename))
    239       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    240                                 "unlink",
    241                                 stage->filename);
    242     GNUNET_free (b);
    243     GNUNET_free (stage->filename);
    244     return false;
    245   }
    246 
    247   {
    248     size_t off = 0;
    249 
    250     while (off < n)
    251     {
    252       ssize_t r;
    253 
    254       r = write (fd,
    255                  b + off,
    256                  n - off);
    257       if (-1 == r)
    258       {
    259         GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    260                                   "write",
    261                                   stage->filename);
    262         GNUNET_break (0 == close (fd));
    263         GNUNET_free (b);
    264         GNUNET_free (stage->filename);
    265         return false;
    266       }
    267       off += r;
    268     }
    269   }
    270   GNUNET_break (0 == close (fd));
    271   GNUNET_free (b);
    272   return true;
    273 }
    274 
    275 
    276 /**
    277  * Generate a response for @a tc indicating an error of type @a ec.
    278  *
    279  * @param[in,out] tc context to fail
    280  * @param ec error code to return
    281  * @param hint hint text to return
    282  */
    283 static void
    284 typst_context_fail (struct TALER_MHD_TypstContext *tc,
    285                     enum TALER_ErrorCode ec,
    286                     const char *hint)
    287 {
    288   struct TALER_MHD_TypstResponse resp = {
    289     .ec = ec,
    290     .details.hint = hint
    291   };
    292 
    293   if (NULL != tc->cb)
    294   {
    295     tc->cb (tc->cb_cls,
    296             &resp);
    297     tc->cb = NULL;
    298   }
    299 }
    300 
    301 
    302 /**
    303  * Helper task for typst_context_fail_async().
    304  *
    305  * @param cls a `struct TALER_MHD_TypstContext`
    306  */
    307 static void
    308 fail_async_cb (void *cls)
    309 {
    310   struct TALER_MHD_TypstContext *tc = cls;
    311 
    312   tc->t = NULL;
    313   typst_context_fail (tc,
    314                       tc->async_ec,
    315                       tc->async_hint);
    316   GNUNET_free (tc->async_hint);
    317   TALER_MHD_typst_cancel (tc);
    318 }
    319 
    320 
    321 /**
    322  * Generate a response for @a tc indicating an error of type @a ec.
    323  *
    324  * @param[in,out] tc context to fail
    325  * @param ec error code to return
    326  * @param hint hint text to return
    327  */
    328 static void
    329 typst_context_fail_async (struct TALER_MHD_TypstContext *tc,
    330                           enum TALER_ErrorCode ec,
    331                           const char *hint)
    332 {
    333   tc->async_ec = ec;
    334   tc->async_hint = (NULL == hint) ? NULL : GNUNET_strdup (hint);
    335   tc->t = GNUNET_SCHEDULER_add_now (&fail_async_cb,
    336                                     tc);
    337 }
    338 
    339 
    340 /**
    341  * Called when the pdftk helper exited.
    342  *
    343  * @param cls our `struct TALER_MHD_TypstContext *`
    344  * @param type type of the process
    345  * @param exit_code status code of the process
    346  */
    347 static void
    348 pdftk_done_cb (void *cls,
    349                enum GNUNET_OS_ProcessStatusType type,
    350                long unsigned int exit_code)
    351 {
    352   struct TALER_MHD_TypstContext *tc = cls;
    353 
    354   tc->cwh = NULL;
    355   GNUNET_process_destroy (tc->proc);
    356   tc->proc = NULL;
    357   switch (type)
    358   {
    359   case GNUNET_OS_PROCESS_UNKNOWN:
    360     GNUNET_assert (0);
    361     return;
    362   case GNUNET_OS_PROCESS_RUNNING:
    363     /* we should not get this notification */
    364     GNUNET_break (0);
    365     return;
    366   case GNUNET_OS_PROCESS_STOPPED:
    367     /* Someone is SIGSTOPing our helper!? */
    368     GNUNET_break (0);
    369     return;
    370   case GNUNET_OS_PROCESS_EXITED:
    371     if (0 != exit_code)
    372     {
    373       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    374                   "pdftk exited with status %d\n",
    375                   (int) exit_code);
    376       typst_context_fail (tc,
    377                           TALER_EC_EXCHANGE_GENERIC_PDFTK_FAILURE,
    378                           "pdftk failed");
    379     }
    380     else
    381     {
    382       struct TALER_MHD_TypstResponse resp = {
    383         .ec = TALER_EC_NONE,
    384         .details.filename = tc->output_file,
    385       };
    386 
    387       GNUNET_assert (NULL != tc->cb);
    388       tc->cb (tc->cb_cls,
    389               &resp);
    390       tc->cb = NULL;
    391     }
    392     break;
    393   case GNUNET_OS_PROCESS_SIGNALED:
    394     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    395                 "pdftk died with signal %d\n",
    396                 (int) exit_code);
    397     typst_context_fail (tc,
    398                         TALER_EC_EXCHANGE_GENERIC_PDFTK_CRASH,
    399                         "pdftk killed by signal");
    400     break;
    401   }
    402   TALER_MHD_typst_cancel (tc);
    403 }
    404 
    405 
    406 /**
    407  * Function called once all of the individual stages are done.
    408  * Triggers the pdftk run for @a tc.
    409  *
    410  * @param[in,out] cls a `struct TALER_MHD_TypstContext *` context to run pdftk for
    411  */
    412 static void
    413 complete_response (void *cls)
    414 {
    415   struct TALER_MHD_TypstContext *tc = cls;
    416   const char *argv[tc->num_stages + 5];
    417 
    418   tc->t = NULL;
    419   argv[0] = "pdftk";
    420   for (unsigned int i = 0; i<tc->num_stages; i++)
    421     argv[i + 1] = tc->stages[i].filename;
    422   argv[tc->num_stages + 1] = "cat";
    423   argv[tc->num_stages + 2] = "output";
    424   argv[tc->num_stages + 3] = tc->output_file;
    425   argv[tc->num_stages + 4] = NULL;
    426   tc->proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
    427   if (GNUNET_OK !=
    428       GNUNET_process_run_command_argv (tc->proc,
    429                                        argv[0],
    430                                        argv))
    431   {
    432     GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
    433                          "fork");
    434     GNUNET_process_destroy (tc->proc);
    435     tc->proc = NULL;
    436     TALER_MHD_typst_cancel (tc);
    437     return;
    438   }
    439   tc->cwh = GNUNET_wait_child (tc->proc,
    440                                &pdftk_done_cb,
    441                                tc);
    442   GNUNET_assert (NULL != tc->cwh);
    443 }
    444 
    445 
    446 /**
    447  * Cancel typst. Wrapper task to do so asynchronously.
    448  *
    449  * @param[in] cls a `struct TALER_MHD_TypstContext`
    450  */
    451 static void
    452 cancel_async (void *cls)
    453 {
    454   struct TALER_MHD_TypstContext *tc = cls;
    455 
    456   tc->t = NULL;
    457   TALER_MHD_typst_cancel (tc);
    458 }
    459 
    460 
    461 /**
    462  * Called when a typst helper exited.
    463  *
    464  * @param cls our `struct TypstStage *`
    465  * @param type type of the process
    466  * @param exit_code status code of the process
    467  */
    468 static void
    469 typst_done_cb (void *cls,
    470                enum GNUNET_OS_ProcessStatusType type,
    471                long unsigned int exit_code)
    472 {
    473   struct TypstStage *stage = cls;
    474   struct TALER_MHD_TypstContext *tc = stage->tc;
    475 
    476   stage->cwh = NULL;
    477   GNUNET_process_destroy (stage->proc);
    478   stage->proc = NULL;
    479   switch (type)
    480   {
    481   case GNUNET_OS_PROCESS_UNKNOWN:
    482     GNUNET_assert (0);
    483     return;
    484   case GNUNET_OS_PROCESS_RUNNING:
    485     /* we should not get this notification */
    486     GNUNET_break (0);
    487     return;
    488   case GNUNET_OS_PROCESS_STOPPED:
    489     /* Someone is SIGSTOPing our helper!? */
    490     GNUNET_break (0);
    491     return;
    492   case GNUNET_OS_PROCESS_EXITED:
    493     if (0 != exit_code)
    494     {
    495       char err[128];
    496 
    497       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    498                   "typst exited with status %d\n",
    499                   (int) exit_code);
    500       GNUNET_snprintf (err,
    501                        sizeof (err),
    502                        "Typst exited with status %d",
    503                        (int) exit_code);
    504       typst_context_fail (tc,
    505                           TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE,
    506                           err);
    507       GNUNET_assert (NULL == tc->t);
    508       tc->t = GNUNET_SCHEDULER_add_now (&cancel_async,
    509                                         tc);
    510       return;
    511     }
    512     break;
    513   case GNUNET_OS_PROCESS_SIGNALED:
    514     {
    515       char err[128];
    516 
    517       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    518                   "typst died with signal %d\n",
    519                   (int) exit_code);
    520       GNUNET_snprintf (err,
    521                        sizeof (err),
    522                        "Typst died with signal %d",
    523                        (int) exit_code);
    524       typst_context_fail (tc,
    525                           TALER_EC_EXCHANGE_GENERIC_TYPST_CRASH,
    526                           err);
    527       GNUNET_assert (NULL == tc->t);
    528       tc->t = GNUNET_SCHEDULER_add_now (&cancel_async,
    529                                         tc);
    530       return;
    531     }
    532     break;
    533   }
    534   tc->active_stages--;
    535   if (NULL != stage->proc)
    536   {
    537     GNUNET_process_destroy (stage->proc);
    538     stage->proc = NULL;
    539   }
    540   if (0 != tc->active_stages)
    541     return;
    542   GNUNET_assert (NULL == tc->t);
    543   tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
    544                                     tc);
    545 }
    546 
    547 
    548 /**
    549  * Check that @a id only consists of characters that are safe to
    550  * interpolate into a Typst `#import` statement (alphanumerics and
    551  * `.`, `-`, `_`).  This prevents code/path injection via the form
    552  * name or version.
    553  *
    554  * @param id identifier to validate
    555  * @return true if @a id is non-empty and only uses the safe charset
    556  */
    557 static bool
    558 is_valid_form_identifier (const char *id)
    559 {
    560   if ( (NULL == id) ||
    561        ('\0' == id[0]) )
    562     return false;
    563   for (const char *p = id; '\0' != *p; p++)
    564   {
    565     char c = *p;
    566 
    567     if (! ( ( ('a' <= c) && ('z' >= c)) ||
    568             ( ('A' <= c) && ('Z' >= c)) ||
    569             ( ('0' <= c) && ('9' >= c)) ||
    570             ('.' == c) ||
    571             ('-' == c) ||
    572             ('_' == c) ) )
    573       return false;
    574   }
    575   return true;
    576 }
    577 
    578 
    579 /**
    580  * Setup typst stage to produce one of the PDF inputs.
    581  *
    582  * @param[out] stage initialized stage data
    583  * @param i index of the stage
    584  * @param tmpdir where to place temporary files
    585  * @param template_path where to find templates
    586  * @param doc input document specification
    587  * @return true on success
    588  */
    589 static bool
    590 setup_stage (struct TypstStage *stage,
    591              unsigned int i,
    592              const char *tmpdir,
    593              const char *template_path,
    594              const struct TALER_MHD_TypstDocument *doc)
    595 {
    596   struct TALER_MHD_TypstContext *tc = stage->tc;
    597   char *input;
    598   bool is_dot_typ;
    599 
    600   if (NULL == doc->form_name)
    601   {
    602     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    603                 "Stage %u: Dumping inlined PDF attachment\n",
    604                 i);
    605     return inline_pdf_stage (stage,
    606                              tmpdir,
    607                              doc->data);
    608   }
    609 
    610   if (! is_valid_form_identifier (doc->form_name))
    611   {
    612     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    613                 "Stage %u: Invalid form name `%s'\n",
    614                 i,
    615                 doc->form_name);
    616     return false;
    617   }
    618   if ( (NULL != doc->form_version) &&
    619        (! is_valid_form_identifier (doc->form_version)) )
    620   {
    621     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    622                 "Stage %u: Invalid form version `%s'\n",
    623                 i,
    624                 doc->form_version);
    625     return false;
    626   }
    627 
    628   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    629               "Stage %u: Handling form %s\n",
    630               i,
    631               doc->form_name);
    632 
    633   /* Setup inputs */
    634   {
    635     char *dirname;
    636 
    637     GNUNET_asprintf (&dirname,
    638                      "%s/%u/",
    639                      tmpdir,
    640                      i);
    641     if (GNUNET_OK !=
    642         GNUNET_DISK_directory_create (dirname))
    643     {
    644       GNUNET_free (dirname);
    645       return false;
    646     }
    647     GNUNET_free (dirname);
    648   }
    649 
    650   /* Setup data input */
    651   {
    652     char *jfn;
    653 
    654     GNUNET_asprintf (&jfn,
    655                      "%s/%u/input.json",
    656                      tmpdir,
    657                      i);
    658     if (0 !=
    659         json_dump_file (doc->data,
    660                         jfn,
    661                         JSON_INDENT (2)))
    662     {
    663       GNUNET_break (0);
    664       GNUNET_free (jfn);
    665       return false;
    666     }
    667     GNUNET_free (jfn);
    668   }
    669 
    670   /* setup output file name */
    671   GNUNET_asprintf (&stage->filename,
    672                    "%s/%u/input.pdf",
    673                    tmpdir,
    674                    i);
    675 
    676   /* setup main input Typst file */
    677   {
    678     char *intyp;
    679     size_t slen = strlen (doc->form_name);
    680 
    681     is_dot_typ = ( (slen > 4) &&
    682                    (0 == memcmp (&doc->form_name[slen - 4],
    683                                  ".typ",
    684                                  4)) );
    685     /* We do not append the ":$VERSION" if a filename ending with ".typ"
    686        is given. Otherwise we append the version, or ":0.0.0" if no
    687        explicit version is given. */
    688     GNUNET_asprintf (&intyp,
    689                      "#import \"%s/%s%s%s\": form\n"
    690                      "#form(json(\"input.json\"))\n",
    691                      template_path,
    692                      doc->form_name,
    693                      is_dot_typ ? "" : ":",
    694                      is_dot_typ
    695                      ? ""
    696                      : ( (NULL == doc->form_version)
    697                          ? "0.0.0"
    698                          : doc->form_version));
    699     GNUNET_asprintf (&input,
    700                      "%s/%u/input.typ",
    701                      tmpdir,
    702                      i);
    703     if (GNUNET_OK !=
    704         GNUNET_DISK_fn_write (input,
    705                               intyp,
    706                               strlen (intyp),
    707                               GNUNET_DISK_PERM_USER_READ))
    708     {
    709       GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    710                                 "write",
    711                                 input);
    712       GNUNET_free (input);
    713       GNUNET_free (intyp);
    714       return false;
    715     }
    716     GNUNET_free (intyp);
    717   }
    718 
    719   /* now setup typst invocation */
    720   {
    721     const char *argv[6];
    722 
    723     if (is_dot_typ)
    724     {
    725       /* This deliberately breaks the typst sandbox. Why? Because Typst sucks
    726          and does not support multiple roots, but here we have dynamic data in
    727          /tmp and a style file outside of /tmp (and copying is also not
    728          practical as we don't know what all to copy). Typst should really
    729          support multiple roots. Anyway, in production this path should not
    730          happen, because there we use Typst packages. */
    731       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    732                   "Bypassing Typst sandbox. You should use Typst packages instead of `%s'.\n",
    733                   doc->form_name);
    734       argv[0] = "typst";
    735       argv[1] = "compile";
    736       argv[2] = "--root";
    737       argv[3] = "/";
    738       argv[4] = input;
    739       argv[5] = NULL;
    740     }
    741     else
    742     {
    743       argv[0] = "typst";
    744       argv[1] = "compile";
    745       argv[2] = input;
    746       argv[3] = NULL;
    747     }
    748     stage->proc = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
    749     {
    750       char *datadir;
    751 
    752       datadir = GNUNET_OS_installation_get_path (
    753         tc->pd,
    754         GNUNET_OS_IPK_DATADIR);
    755       GNUNET_assert (GNUNET_OK ==
    756                      GNUNET_process_set_options (
    757                        stage->proc,
    758                        GNUNET_process_option_set_environment ("XDG_DATA_HOME",
    759                                                               datadir)));
    760       GNUNET_free (datadir);
    761     }
    762     if (GNUNET_OK !=
    763         GNUNET_process_run_command_argv (stage->proc,
    764                                          "typst",
    765                                          argv))
    766     {
    767       GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
    768                            "fork");
    769       GNUNET_free (input);
    770       GNUNET_process_destroy (stage->proc);
    771       stage->proc = NULL;
    772       return false;
    773     }
    774     GNUNET_free (input);
    775     tc->active_stages++;
    776     stage->cwh = GNUNET_wait_child (stage->proc,
    777                                     &typst_done_cb,
    778                                     stage);
    779     GNUNET_assert (NULL != stage->cwh);
    780   }
    781   return true;
    782 }
    783 
    784 
    785 struct TALER_MHD_TypstContext *
    786 TALER_MHD_typst (
    787   const struct GNUNET_OS_ProjectData *pd,
    788   const struct GNUNET_CONFIGURATION_Handle *cfg,
    789   bool remove_on_exit,
    790   const char *cfg_section_name,
    791   unsigned int num_documents,
    792   const struct TALER_MHD_TypstDocument docs[static num_documents],
    793   TALER_MHD_TypstResultCallback cb,
    794   void *cb_cls)
    795 {
    796   static enum GNUNET_GenericReturnValue once = GNUNET_NO;
    797   struct TALER_MHD_TypstContext *tc;
    798 
    799   switch (once)
    800   {
    801   case GNUNET_OK:
    802     break;
    803   case GNUNET_NO:
    804     if (GNUNET_SYSERR ==
    805         GNUNET_OS_check_helper_binary ("typst",
    806                                        false,
    807                                        NULL))
    808     {
    809       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    810                   "`typst' command not found\n");
    811       once = GNUNET_SYSERR;
    812       return NULL;
    813     }
    814     if (GNUNET_SYSERR ==
    815         GNUNET_OS_check_helper_binary ("pdftk",
    816                                        false,
    817                                        NULL))
    818     {
    819       GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    820                   "`pdftk' command not found\n");
    821       once = GNUNET_SYSERR;
    822       return NULL;
    823     }
    824     once = GNUNET_OK;
    825     break;
    826   case GNUNET_SYSERR:
    827     GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
    828                 "PDF generation initialization failed before, not even trying again\n");
    829     return NULL;
    830   }
    831   tc = GNUNET_new (struct TALER_MHD_TypstContext);
    832   tc->pd = pd;
    833   tc->tmpdir = GNUNET_strdup ("/tmp/taler-typst-XXXXXX");
    834   tc->remove_on_exit = remove_on_exit;
    835   tc->cb = cb;
    836   tc->cb_cls = cb_cls;
    837   if (NULL == mkdtemp (tc->tmpdir))
    838   {
    839     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR,
    840                               "mkdtemp",
    841                               tc->tmpdir);
    842     GNUNET_free (tc->tmpdir);
    843     TALER_MHD_typst_cancel (tc);
    844     return NULL;
    845   }
    846   GNUNET_asprintf (&tc->output_file,
    847                    "%s/final.pdf",
    848                    tc->tmpdir);
    849 
    850   /* setup typst stages */
    851   {
    852     char *template_path;
    853 
    854     if (GNUNET_OK !=
    855         GNUNET_CONFIGURATION_get_value_string (cfg,
    856                                                cfg_section_name,
    857                                                "TYPST_TEMPLATES",
    858                                                &template_path))
    859     {
    860       GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
    861                                  cfg_section_name,
    862                                  "TYPST_TEMPLATES");
    863       TALER_MHD_typst_cancel (tc);
    864       return NULL;
    865     }
    866     if ('@' != template_path[0])
    867       template_path = GNUNET_CONFIGURATION_expand_dollar (cfg,
    868                                                           template_path);
    869     tc->stages = GNUNET_new_array (num_documents,
    870                                    struct TypstStage);
    871     tc->num_stages = num_documents;
    872     for (unsigned int i = 0; i<num_documents; i++)
    873     {
    874       tc->stages[i].tc = tc;
    875       if (! setup_stage (&tc->stages[i],
    876                          i,
    877                          tc->tmpdir,
    878                          template_path,
    879                          &docs[i]))
    880       {
    881         char err[128];
    882 
    883         GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    884                     "Typst setup failed on stage %u\n",
    885                     i);
    886         GNUNET_snprintf (err,
    887                          sizeof (err),
    888                          "Typst setup failed on stage %u",
    889                          i);
    890         typst_context_fail_async (tc,
    891                                   TALER_EC_EXCHANGE_GENERIC_TYPST_TEMPLATE_FAILURE,
    892                                   err);
    893         return tc;
    894       }
    895     }
    896     GNUNET_free (template_path);
    897   }
    898   if (0 == tc->active_stages)
    899   {
    900     tc->t = GNUNET_SCHEDULER_add_now (&complete_response,
    901                                       tc);
    902   }
    903   return tc;
    904 }
    905 
    906 
    907 struct MHD_Response *
    908 TALER_MHD_response_from_pdf_file (const char *filename)
    909 {
    910   struct MHD_Response *resp;
    911   struct stat s;
    912   int fd;
    913 
    914   fd = open (filename,
    915              O_RDONLY);
    916   if (-1 == fd)
    917   {
    918     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    919                               "open",
    920                               filename);
    921     return NULL;
    922   }
    923   if (0 !=
    924       fstat (fd,
    925              &s))
    926   {
    927     GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING,
    928                               "fstat",
    929                               filename);
    930     GNUNET_assert (0 == close (fd));
    931     return NULL;
    932   }
    933   resp = MHD_create_response_from_fd (s.st_size,
    934                                       fd);
    935   TALER_MHD_add_global_headers (resp,
    936                                 false);
    937   GNUNET_break (MHD_YES ==
    938                 MHD_add_response_header (resp,
    939                                          MHD_HTTP_HEADER_CONTENT_TYPE,
    940                                          "application/pdf"));
    941   return resp;
    942 }