Coverage Report

Created: 2026-08-31 06:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/httrack/src/htswarc.c
Line
Count
Source
1
/* ------------------------------------------------------------ */
2
/*
3
HTTrack Website Copier, Offline Browser for Windows and Unix
4
Copyright (C) 2026 Xavier Roche and other contributors
5
6
SPDX-License-Identifier: GPL-3.0-or-later
7
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
12
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
GNU General Public License for more details.
17
18
You should have received a copy of the GNU General Public License
19
along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21
Ethical use: we kindly ask that you NOT use this software to harvest email
22
addresses or to collect any other private information about people. Doing so
23
would dishonor our work and waste the many hours we have spent on it.
24
25
Please visit our Website: http://www.httrack.com
26
*/
27
28
/* ------------------------------------------------------------ */
29
/* HTTrack WARC/1.1 output writer (ISO 28500). See warc.h.
30
   The response record stores the body verbatim, as received: Content-Encoding
31
   kept, only the hop-by-hop Transfer-Encoding dropped, Content-Length rewritten
32
   to the stored length (like wget --warc / Heritrix). */
33
/* ------------------------------------------------------------ */
34
35
#define HTS_INTERNAL_BYTECODE
36
37
#include "htswarc.h"
38
39
#include "htscore.h"
40
#include "htslib.h"
41
#include "htsio.h"
42
#include "htsback.h"
43
#include "htstools.h"
44
#include "htssafe.h"
45
#include "htszlib.h"
46
#include "coucal/coucal.h"
47
48
#include <ctype.h>
49
#include <stdarg.h>
50
#include <stdint.h>
51
#include <stdlib.h>
52
#include <time.h>
53
54
#if HTS_USEOPENSSL
55
#include <openssl/evp.h>
56
#include <openssl/rand.h>
57
#endif
58
59
/* opt->state.warc value meaning "no writer, and do not open one": the open
60
   failed, or the session is done with the archive. */
61
0
#define WARC_DISABLED ((void *) ~(uintptr_t) 0)
62
63
/* Suffix of the in-progress archive when a previous one must survive it. */
64
#define WARC_TMP_SUFFIX ".tmp"
65
66
struct warc_writer {
67
  FILE *f;
68
  httrackp *opt;   /* kept for close-time logging (warc_wacz_package) */
69
  int gz;          /* 1: one gzip member per record; 0: raw */
70
  uint64_t offset; /* running byte offset (member starts, for a future index) */
71
  uint64_t counter; /* monotonic record counter */
72
  uint64_t rng;     /* PRNG state for the UUID fallback */
73
  char info_id[64]; /* warcinfo WARC-Record-ID, referenced by every record */
74
  coucal seen;      /* base32 payload digest -> "uri\001date" (revisit dedup) */
75
  /* --warc-max-size rotation: NAME-00000.warc.gz, -00001, ... (wget-style). */
76
  uint64_t max_size;   /* rotate once a segment reaches this; 0: single file */
77
  char *seg_base;      /* segment path without the .warc[.gz] suffix, or NULL */
78
  const char *seg_ext; /* ".warc.gz" or ".warc" */
79
  unsigned seg;        /* current segment number */
80
  char *info_fields;   /* warcinfo body, re-emitted at each new segment */
81
  /* --warc-cdx: accumulate one CDXJ line per response/revisit/resource record,
82
     sorted (LC_ALL=C) and written to <base>.cdx at close. */
83
  int cdx_on;       /* --warc-cdx enabled */
84
  char *cdx_path;   /* <base>.cdx output path, or NULL */
85
  char *cur_seg;    /* basename of the current segment file (CDXJ filename) */
86
  char **cdx_lines; /* NUL-terminated CDXJ lines (no newline), owned */
87
  size_t cdx_count; /* lines in use */
88
  size_t cdx_cap;   /* lines allocated */
89
  /* --wacz: at crawl end, package the segment(s) + .cdx + a generated
90
     pages.jsonl into <base>.wacz (WACZ 1.1.1). SHA-256 needs OpenSSL. */
91
  int wacz_on;          /* --wacz enabled (and OpenSSL present) */
92
  char *base_path;      /* resolved archive path minus .warc[.gz] suffix */
93
  const char *base_ext; /* ".warc.gz" or ".warc" (static) */
94
  char *arc_path;    /* full single-file archive path (NULL under rotation) */
95
  /* A re-run must not destroy an archive it cannot replace (#759). */
96
  hts_boolean protect_prev;   /* previous archive present: build in a temp */
97
  hts_boolean opened;         /* open completed; a failed one swaps nothing */
98
  hts_boolean failed;         /* a record or segment was lost: swap nothing */
99
  uint64_t unbacked_revisits; /* URLs this pass didn't capture here: an
100
                                  unbacked revisit, or nothing written at all */
101
  char **page_lines; /* one JSON page line per 200 text/html response, owned */
102
  size_t page_count;
103
  size_t page_cap;
104
  char *main_url;  /* first captured page URL (datapackage mainPageUrl) */
105
  char *main_date; /* its WARC-Date (mainPageDate) */
106
};
107
108
/* A 32-bit offset misindexes every record past 4GB, and `unsigned long` is
109
   that narrow on LLP64: pin the width, not the spelling. */
110
HTS_STATIC_ASSERT(sizeof(((struct warc_writer *) 0)->offset) >= 8,
111
                  warc_offset_is_64bit);
112
113
0
const char *warc_truncated_reason(int code) {
114
0
  switch (code) {
115
0
  case WARC_TRUNC_LENGTH:
116
0
    return "length";
117
0
  case WARC_TRUNC_TIME:
118
0
    return "time";
119
0
  case WARC_TRUNC_DISCONNECT:
120
0
    return "disconnect";
121
0
  default:
122
0
    return NULL;
123
0
  }
124
0
}
125
126
/* ---- growable byte buffer (overflow-safe, project allocators) ---- */
127
128
typedef struct {
129
  char *data;
130
  size_t len;
131
  size_t cap;
132
} wbuf;
133
134
0
static void wbuf_free(wbuf *b) {
135
0
  freet(b->data);
136
0
  b->len = b->cap = 0;
137
0
}
138
139
/* Make room for n more bytes; returns 0 on success, -1 on OOM/overflow. */
140
0
static int wbuf_reserve(wbuf *b, size_t n) {
141
0
  size_t ncap;
142
0
  char *nd;
143
0
  if (n > (size_t) -1 - b->len)
144
0
    return -1;
145
0
  if (b->len + n <= b->cap)
146
0
    return 0;
147
0
  ncap = b->cap ? b->cap : 256;
148
0
  while (ncap < b->len + n) {
149
0
    if (ncap > (size_t) -1 / 2)
150
0
      return -1;
151
0
    ncap *= 2;
152
0
  }
153
0
  nd = realloct(b->data, ncap);
154
0
  if (nd == NULL)
155
0
    return -1;
156
0
  b->data = nd;
157
0
  b->cap = ncap;
158
0
  return 0;
159
0
}
160
161
/* Append n bytes; returns 0 on success, -1 on OOM/overflow. */
162
0
static int wbuf_add(wbuf *b, const void *p, size_t n) {
163
0
  if (wbuf_reserve(b, n) != 0)
164
0
    return -1;
165
0
  memcpy(b->data + b->len, p, n);
166
0
  b->len += n;
167
0
  return 0;
168
0
}
169
170
0
static int wbuf_puts(wbuf *b, const char *s) {
171
0
  return wbuf_add(b, s, strlen(s));
172
0
}
173
174
static int wbuf_printf(wbuf *b, const char *fmt, ...) HTS_PRINTF_FUN(2, 3);
175
176
/* A header line carrying a URL has no useful bound, and giving up here loses
177
   the whole record, so anything past the stack buffer is formatted into the
178
   wbuf itself rather than rejected (#785). */
179
0
static int wbuf_printf(wbuf *b, const char *fmt, ...) {
180
0
  char tmp[1024];
181
0
  size_t need;
182
0
  int n;
183
0
  va_list ap;
184
0
  va_start(ap, fmt);
185
0
  n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
186
0
  va_end(ap);
187
0
  if (n < 0)
188
0
    return -1;
189
0
  if ((size_t) n < sizeof(tmp))
190
0
    return wbuf_add(b, tmp, (size_t) n);
191
0
  need = (size_t) n + 1; /* +1: vsnprintf always writes the NUL */
192
0
  if (wbuf_reserve(b, need) != 0)
193
0
    return -1;
194
0
  va_start(ap, fmt);
195
0
  n = vsnprintf(b->data + b->len, need, fmt, ap);
196
0
  va_end(ap);
197
  /* Never advance past what was reserved, whatever the second pass returns. */
198
0
  if (n < 0 || (size_t) n >= need)
199
0
    return -1;
200
0
  b->len += (size_t) n; /* the NUL is scratch, overwritten by the next append */
201
0
  return 0;
202
0
}
203
204
/* ---- gzip-per-record member writer (mirrors ae_write_packed) ---- */
205
206
typedef struct {
207
  warc_writer *w;
208
  z_stream strm;
209
  int active; /* deflate stream initialized */
210
} member;
211
212
0
static int member_begin(member *m, warc_writer *w) {
213
0
  m->w = w;
214
0
  m->active = 0;
215
0
  if (w->gz) {
216
0
    memset(&m->strm, 0, sizeof(m->strm));
217
    /* windowBits=31 => full RFC1952 gzip member */
218
0
    if (deflateInit2(&m->strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8,
219
0
                     Z_DEFAULT_STRATEGY) != Z_OK)
220
0
      return -1;
221
0
    m->active = 1;
222
0
  }
223
0
  return 0;
224
0
}
225
226
0
static int member_write(member *m, const void *p, size_t n) {
227
0
  if (!m->w->gz)
228
0
    return (n == 0 || hts_fwrite_exact(p, n, m->w->f)) ? 0 : -1;
229
0
  m->strm.next_in = (const Bytef *) p;
230
0
  while (n > 0) {
231
0
    unsigned char out[8192];
232
0
    size_t got;
233
0
    uInt chunk = (n > (uInt) -1) ? (uInt) -1 : (uInt) n;
234
0
    m->strm.avail_in = chunk;
235
0
    do {
236
0
      m->strm.next_out = out;
237
0
      m->strm.avail_out = sizeof(out);
238
0
      if (deflate(&m->strm, Z_NO_FLUSH) != Z_OK)
239
0
        return -1;
240
0
      got = sizeof(out) - m->strm.avail_out;
241
0
      if (got > 0 && !hts_fwrite_exact(out, got, m->w->f))
242
0
        return -1;
243
0
    } while (m->strm.avail_out == 0);
244
0
    n -= chunk;
245
0
  }
246
0
  return 0;
247
0
}
248
249
0
static int member_end(member *m) {
250
0
  int rc = 0;
251
0
  if (m->active) {
252
0
    unsigned char out[8192];
253
0
    int zerr;
254
0
    m->strm.avail_in = 0;
255
0
    do {
256
0
      m->strm.next_out = out;
257
0
      m->strm.avail_out = sizeof(out);
258
0
      zerr = deflate(&m->strm, Z_FINISH);
259
0
      {
260
0
        size_t got = sizeof(out) - m->strm.avail_out;
261
0
        if (got > 0 && !hts_fwrite_exact(out, got, m->w->f))
262
0
          rc = -1;
263
0
      }
264
0
    } while (zerr == Z_OK);
265
0
    if (zerr != Z_STREAM_END)
266
0
      rc = -1;
267
0
    deflateEnd(&m->strm);
268
0
    m->active = 0;
269
0
  }
270
0
  return rc;
271
0
}
272
273
/* ---- SHA-1 + Base32 (digests are OpenSSL-only; omitted otherwise) ---- */
274
275
#if HTS_USEOPENSSL
276
/* Streaming SHA-1 over the block (all regions) and the payload (body only). */
277
typedef struct {
278
  EVP_MD_CTX *block;
279
  EVP_MD_CTX *payload;
280
} digester;
281
282
static void base32_20(const unsigned char in[20], char out[33]) {
283
  static const char a[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
284
  int i, o = 0;
285
  uint64_t buf = 0;
286
  int bits = 0;
287
  for (i = 0; i < 20; i++) {
288
    buf = (buf << 8) | in[i];
289
    bits += 8;
290
    while (bits >= 5) {
291
      bits -= 5;
292
      out[o++] = a[(buf >> bits) & 0x1F];
293
    }
294
  }
295
  out[o] = '\0'; /* 20 bytes => exactly 32 base32 chars, no padding */
296
}
297
#endif
298
299
/* Stream a block to a sink. When http_section, the HTTP header bytes (may be
300
   empty) plus their terminating CRLF are emitted first; the separator is bound
301
   to http_section, not to http_hdr being non-NULL, so an empty header still
302
   emits (and is counted in) the 2-byte separator (F3). The on-disk body is
303
   written as EXACTLY body_len octets — capped if the file grew, zero-padded if
304
   it shrank — so the declared Content-Length always equals the bytes written
305
   across every pass (F2). region 0=header, 1=body. Returns 0 on success. */
306
typedef int (*warc_sink)(void *ctx, int region, const void *p, size_t n);
307
308
0
static int stream_body_pad(warc_sink sink, void *ctx, size_t remaining) {
309
0
  static const char zeros[4096] = {0};
310
0
  while (remaining > 0) {
311
0
    size_t chunk = (remaining < sizeof(zeros)) ? remaining : sizeof(zeros);
312
0
    if (sink(ctx, 1, zeros, chunk) != 0)
313
0
      return -1;
314
0
    remaining -= chunk;
315
0
  }
316
0
  return 0;
317
0
}
318
319
static int stream_block(int http_section, const char *http_hdr,
320
                        size_t http_hdr_len, int has_body, const char *body,
321
                        size_t body_len, const char *body_path, warc_sink sink,
322
0
                        void *ctx) {
323
0
  if (http_section) {
324
0
    if (http_hdr != NULL && http_hdr_len > 0 &&
325
0
        sink(ctx, 0, http_hdr, http_hdr_len) != 0)
326
0
      return -1;
327
0
    if (sink(ctx, 0, "\r\n", 2) != 0)
328
0
      return -1;
329
0
  }
330
0
  if (has_body) {
331
0
    if (body != NULL) {
332
0
      if (body_len > 0 && sink(ctx, 1, body, body_len) != 0)
333
0
        return -1;
334
0
    } else if (body_path != NULL) {
335
0
      char catbuff[CATBUFF_SIZE];
336
0
      size_t remaining = body_len;
337
0
      FILE *fp = FOPEN(fconv(catbuff, sizeof(catbuff), body_path), "rb");
338
0
      if (fp == NULL)
339
0
        return -1;
340
0
      while (remaining > 0) {
341
0
        char b[32768];
342
0
        size_t want = (remaining < sizeof(b)) ? remaining : sizeof(b);
343
0
        size_t nl = fread(b, 1, want, fp);
344
0
        if (nl == 0)
345
0
          break; /* short file: pad below so written == declared */
346
0
        if (sink(ctx, 1, b, nl) != 0) {
347
0
          fclose(fp);
348
0
          return -1;
349
0
        }
350
0
        remaining -= nl;
351
0
      }
352
0
      fclose(fp);
353
0
      if (stream_body_pad(sink, ctx, remaining) != 0)
354
0
        return -1;
355
0
    }
356
0
  }
357
0
  return 0;
358
0
}
359
360
#if HTS_USEOPENSSL
361
static int digest_sink(void *ctx, int region, const void *p, size_t n) {
362
  digester *d = (digester *) ctx;
363
  if (d->block != NULL && EVP_DigestUpdate(d->block, p, n) != 1)
364
    return -1;
365
  if (region == 1 && d->payload != NULL &&
366
      EVP_DigestUpdate(d->payload, p, n) != 1)
367
    return -1;
368
  return 0;
369
}
370
#endif
371
372
0
static int write_sink(void *ctx, int region, const void *p, size_t n) {
373
0
  (void) region;
374
0
  return member_write((member *) ctx, p, n);
375
0
}
376
377
/* Base32 SHA-1 of a transaction payload (body only), for revisit dedup.
378
   Returns 1 and fills out[33] on success, 0 without OpenSSL or on error. */
379
static int payload_digest_b32(const char *body, size_t body_len,
380
0
                              const char *body_path, char out[33]) {
381
#if HTS_USEOPENSSL
382
  digester d;
383
  unsigned char md[EVP_MAX_MD_SIZE];
384
  unsigned int mdlen = 0;
385
  int ok;
386
  d.block = NULL;
387
  d.payload = EVP_MD_CTX_new();
388
  if (d.payload == NULL)
389
    return 0;
390
  if (EVP_DigestInit_ex(d.payload, EVP_sha1(), NULL) != 1) {
391
    EVP_MD_CTX_free(d.payload);
392
    return 0;
393
  }
394
  ok = stream_block(0, NULL, 0, 1, body, body_len, body_path, digest_sink,
395
                    &d) == 0 &&
396
       EVP_DigestFinal_ex(d.payload, md, &mdlen) == 1 && mdlen == 20;
397
  EVP_MD_CTX_free(d.payload);
398
  if (!ok)
399
    return 0;
400
  base32_20(md, out);
401
  return 1;
402
#else
403
0
  (void) body;
404
0
  (void) body_len;
405
0
  (void) body_path;
406
0
  (void) out;
407
0
  return 0;
408
0
#endif
409
0
}
410
411
/* ---- SHA-256 hex (WACZ digests; OpenSSL-only) ---- */
412
413
#if HTS_USEOPENSSL
414
static void md32_to_hex(const unsigned char md[32], char out[65]) {
415
  static const char hx[] = "0123456789abcdef";
416
  int i;
417
  for (i = 0; i < 32; i++) {
418
    out[i * 2] = hx[md[i] >> 4];
419
    out[i * 2 + 1] = hx[md[i] & 0x0F];
420
  }
421
  out[64] = '\0';
422
}
423
424
/* Lowercase-hex SHA-256 of n bytes at p into out[65]. Returns 1 on success. */
425
static int sha256_hex_mem(const void *p, size_t n, char out[65]) {
426
  EVP_MD_CTX *c = EVP_MD_CTX_new();
427
  unsigned char md[EVP_MAX_MD_SIZE];
428
  unsigned int mdlen = 0;
429
  int ok;
430
  if (c == NULL)
431
    return 0;
432
  ok = EVP_DigestInit_ex(c, EVP_sha256(), NULL) == 1 &&
433
       (n == 0 || EVP_DigestUpdate(c, p, n) == 1) &&
434
       EVP_DigestFinal_ex(c, md, &mdlen) == 1 && mdlen == 32;
435
  EVP_MD_CTX_free(c);
436
  if (ok)
437
    md32_to_hex(md, out);
438
  return ok;
439
}
440
441
/* Lowercase-hex SHA-256 of the on-disk file at path into out[65]. */
442
static int sha256_hex_file(const char *path, char out[65]) {
443
  EVP_MD_CTX *c;
444
  FILE *fp;
445
  char catbuff[CATBUFF_SIZE];
446
  unsigned char md[EVP_MAX_MD_SIZE];
447
  unsigned int mdlen = 0;
448
  int ok;
449
  fp = FOPEN(fconv(catbuff, sizeof(catbuff), path), "rb");
450
  if (fp == NULL)
451
    return 0;
452
  c = EVP_MD_CTX_new();
453
  if (c == NULL) {
454
    fclose(fp);
455
    return 0;
456
  }
457
  ok = EVP_DigestInit_ex(c, EVP_sha256(), NULL) == 1;
458
  while (ok) {
459
    unsigned char b[32768];
460
    size_t nl = fread(b, 1, sizeof(b), fp);
461
    if (nl > 0 && EVP_DigestUpdate(c, b, nl) != 1)
462
      ok = 0;
463
    if (nl < sizeof(b))
464
      break;
465
  }
466
  ok = ok && !ferror(fp) && EVP_DigestFinal_ex(c, md, &mdlen) == 1 &&
467
       mdlen == 32;
468
  EVP_MD_CTX_free(c);
469
  fclose(fp);
470
  if (ok)
471
    md32_to_hex(md, out);
472
  return ok;
473
}
474
#endif
475
476
/* ---- misc record helpers ---- */
477
478
0
static void warc_fill_random(warc_writer *w, unsigned char *b, size_t n) {
479
0
  size_t i;
480
#if HTS_USEOPENSSL
481
  if (n <= (size_t) 0x7fffffff && RAND_bytes(b, (int) n) == 1)
482
    return;
483
#endif
484
0
  for (i = 0; i < n; i++) {
485
0
    w->rng ^= w->rng << 13;
486
0
    w->rng ^= w->rng >> 7;
487
0
    w->rng ^= w->rng << 17;
488
0
    b[i] = (unsigned char) (w->rng >> 24);
489
0
  }
490
0
}
491
492
/* urn:uuid: v4-shaped record id (uniqueness within the run is what matters). */
493
0
static void warc_make_id(warc_writer *w, char out[64]) {
494
0
  unsigned char b[16];
495
0
  w->counter++;
496
0
  warc_fill_random(w, b, sizeof(b));
497
0
  b[6] = (unsigned char) ((b[6] & 0x0F) | 0x40);
498
0
  b[8] = (unsigned char) ((b[8] & 0x3F) | 0x80);
499
0
  snprintf(out, 64,
500
0
           "<urn:uuid:%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-"
501
0
           "%02x%02x%02x%02x%02x%02x>",
502
0
           b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10],
503
0
           b[11], b[12], b[13], b[14], b[15]);
504
0
}
505
506
/* Case-insensitive "is this the header named name?" test, tolerating optional
507
   whitespace before the ':' (non-compliant "Name : value" is still matched). */
508
0
static int header_is(const char *line, size_t line_len, const char *name) {
509
0
  size_t nl = strlen(name), i;
510
0
  if (line_len < nl || strncasecmp(line, name, nl) != 0)
511
0
    return 0;
512
0
  for (i = nl; i < line_len && (line[i] == ' ' || line[i] == '\t'); i++)
513
0
    ;
514
0
  return i < line_len && line[i] == ':';
515
0
}
516
517
/* Build the normalized HTTP header block from raw resp_hdr into out (no
518
   trailing CRLF terminator). Always drops the hop-by-hop Transfer-Encoding.
519
   When set_cl>=0 (a body is stored), drops the original Content-Length and
520
   appends "Content-Length: <set_cl>" (the stored, coded length) while keeping
521
   every Content-Encoding line verbatim: a faithful archive of what the server
522
   sent. Returns 0. */
523
static int normalize_http_headers(const char *resp_hdr, long long set_cl,
524
0
                                  wbuf *out) {
525
0
  const char *p = resp_hdr;
526
0
  int first = 1;
527
0
  if (resp_hdr == NULL)
528
0
    return -1;
529
0
  while (*p != '\0') {
530
0
    const char *eol = strchr(p, '\n');
531
0
    size_t len = (eol != NULL) ? (size_t) (eol - p) : strlen(p);
532
0
    if (len > 0 && p[len - 1] == '\r')
533
0
      len--; /* strip CR; re-added as CRLF below */
534
0
    if (len == 0)
535
0
      break; /* blank line: end of headers */
536
0
    if (first) {
537
0
      first = 0; /* status line: keep verbatim */
538
0
    } else if (header_is(p, len, "Transfer-Encoding")) {
539
0
      goto next;
540
0
    } else if (set_cl >= 0 && header_is(p, len, "Content-Length")) {
541
0
      goto next;
542
0
    }
543
0
    if (wbuf_add(out, p, len) != 0 || wbuf_add(out, "\r\n", 2) != 0)
544
0
      return -1;
545
0
  next:
546
0
    if (eol == NULL)
547
0
      break;
548
0
    p = eol + 1;
549
0
  }
550
0
  if (set_cl >= 0 && wbuf_printf(out, "Content-Length: %lld\r\n", set_cl) != 0)
551
0
    return -1;
552
0
  return 0;
553
0
}
554
555
/* ---- CDXJ index (--warc-cdx) ---- */
556
557
/* Duplicate the last path component (basename), or NULL on OOM. */
558
0
static char *path_basename_dup(const char *path) {
559
0
  const char *b = path, *p;
560
0
  for (p = path; *p != '\0'; p++)
561
0
    if (*p == '/' || *p == '\\')
562
0
      b = p + 1;
563
0
  return strdupt(b);
564
0
}
565
566
/* SURT-canonicalize url into out (no newline): scheme and userinfo dropped,
567
   host lowercased with a leading www[digits] label stripped and the scheme
568
   default port removed, labels reversed and comma-joined then ')', path+query
569
   appended verbatim (case preserved, fragment dropped). IPv4 and [IPv6]
570
   literals keep their host form. A non-default port is kept as ":port" before
571
   the ')'. Returns 0 on success. */
572
0
static int surt_canon(const char *url, wbuf *out) {
573
0
  const char *p, *scheme_end, *authend, *host, *hostsep, *port = NULL;
574
0
  size_t portlen = 0, hlen, i;
575
0
  int def_port = -1;
576
0
  int is_ipv6 = 0, is_ip = 0;
577
0
  char hostbuf[1024];
578
579
0
  if (url == NULL)
580
0
    return -1;
581
582
0
  p = url;
583
0
  scheme_end = strstr(url, "://");
584
0
  if (scheme_end != NULL) {
585
0
    size_t sl = (size_t) (scheme_end - url);
586
0
    if (sl == 4 && strncasecmp(url, "http", 4) == 0)
587
0
      def_port = 80;
588
0
    else if (sl == 5 && strncasecmp(url, "https", 5) == 0)
589
0
      def_port = 443;
590
0
    p = scheme_end + 3;
591
0
  }
592
593
0
  authend = p;
594
0
  while (*authend != '\0' && *authend != '/' && *authend != '?' &&
595
0
         *authend != '#')
596
0
    authend++;
597
598
0
  { /* drop userinfo up to the last '@' inside the authority */
599
0
    const char *q, *at = NULL;
600
0
    for (q = p; q < authend; q++)
601
0
      if (*q == '@')
602
0
        at = q;
603
0
    if (at != NULL)
604
0
      p = at + 1;
605
0
  }
606
0
  host = p;
607
608
0
  if (host < authend && host[0] == '[') { /* [IPv6] literal */
609
0
    const char *rb = host;
610
0
    is_ipv6 = 1;
611
0
    while (rb < authend && *rb != ']')
612
0
      rb++;
613
0
    hostsep = (rb < authend) ? rb + 1 : authend;
614
0
  } else {
615
0
    const char *c = host;
616
0
    while (c < authend && *c != ':')
617
0
      c++;
618
0
    hostsep = c;
619
0
  }
620
0
  hlen = (size_t) (hostsep - host);
621
0
  if (hostsep < authend && *hostsep == ':') {
622
0
    port = hostsep + 1;
623
0
    portlen = (size_t) (authend - port);
624
0
  }
625
626
0
  if (hlen >= sizeof(hostbuf))
627
0
    return -1;
628
0
  for (i = 0; i < hlen; i++)
629
0
    hostbuf[i] = (char) tolower((unsigned char) host[i]);
630
0
  hostbuf[hlen] = '\0';
631
632
0
  if (!is_ipv6)
633
0
    is_ip = hts_host_is_ipv4(hostbuf, hlen);
634
635
0
  if (!is_ipv6 && !is_ip && hlen >= 4 && hostbuf[0] == 'w' &&
636
0
      hostbuf[1] == 'w' && hostbuf[2] == 'w') {
637
0
    size_t k = 3;
638
0
    while (k < hlen && hostbuf[k] >= '0' && hostbuf[k] <= '9')
639
0
      k++;
640
0
    if (k < hlen && hostbuf[k] == '.') {
641
0
      memmove(hostbuf, hostbuf + k + 1, hlen - (k + 1));
642
0
      hlen -= k + 1;
643
0
      hostbuf[hlen] = '\0';
644
0
    }
645
0
  }
646
647
0
  if (is_ipv6 || is_ip) {
648
0
    if (wbuf_add(out, hostbuf, hlen) != 0)
649
0
      return -1;
650
0
  } else { /* reverse the dot-separated labels, comma-joined */
651
0
    long idx;
652
0
    size_t seg_end = hlen;
653
0
    int first = 1;
654
0
    for (idx = (long) hlen; idx >= 0; idx--) {
655
0
      if (idx == 0 || hostbuf[idx - 1] == '.') {
656
0
        size_t lstart = (size_t) idx;
657
0
        size_t llen = seg_end - lstart;
658
0
        if (llen > 0) {
659
0
          if (!first && wbuf_add(out, ",", 1) != 0)
660
0
            return -1;
661
0
          if (wbuf_add(out, hostbuf + lstart, llen) != 0)
662
0
            return -1;
663
0
          first = 0;
664
0
        }
665
0
        seg_end = (idx > 0) ? (size_t) (idx - 1) : 0;
666
0
      }
667
0
    }
668
0
  }
669
670
0
  if (port != NULL && portlen > 0) { /* keep a non-default port */
671
0
    int pv = 0, ok = 1;
672
0
    size_t k;
673
0
    for (k = 0; k < portlen; k++) {
674
0
      if (port[k] < '0' || port[k] > '9') {
675
0
        ok = 0;
676
0
        break;
677
0
      }
678
0
      pv = pv * 10 + (port[k] - '0');
679
0
    }
680
0
    if (ok && pv != def_port &&
681
0
        (wbuf_add(out, ":", 1) != 0 || wbuf_add(out, port, portlen) != 0))
682
0
      return -1;
683
0
  }
684
685
0
  if (wbuf_add(out, ")", 1) != 0)
686
0
    return -1;
687
688
0
  { /* path + query, verbatim up to any fragment */
689
0
    const char *frag = authend;
690
0
    while (*frag != '\0' && *frag != '#')
691
0
      frag++;
692
0
    if (wbuf_add(out, authend, (size_t) (frag - authend)) != 0)
693
0
      return -1;
694
0
  }
695
0
  return 0;
696
0
}
697
698
/* 14-digit YYYYMMDDhhmmss from a WARC-Date "YYYY-MM-DDThh:mm:ssZ" (digits
699
 * only). */
700
0
static void iso8601_to_cdx14(const char *iso, char out[15]) {
701
0
  int o = 0;
702
0
  const char *p;
703
0
  for (p = iso; *p != '\0' && o < 14; p++)
704
0
    if (*p >= '0' && *p <= '9')
705
0
      out[o++] = *p;
706
0
  while (o < 14)
707
0
    out[o++] = '0';
708
0
  out[14] = '\0';
709
0
}
710
711
/* Append s as a JSON string (quoted, with " \ and control chars escaped). */
712
0
static int cdx_json_str(wbuf *b, const char *s) {
713
0
  if (wbuf_add(b, "\"", 1) != 0)
714
0
    return -1;
715
0
  for (; *s != '\0'; s++) {
716
0
    unsigned char c = (unsigned char) *s;
717
0
    if (c == '"' || c == '\\') {
718
0
      char e[2] = {'\\', (char) c};
719
0
      if (wbuf_add(b, e, 2) != 0)
720
0
        return -1;
721
0
    } else if (c < 0x20) {
722
0
      if (wbuf_printf(b, "\\u%04x", (unsigned) c) != 0)
723
0
        return -1;
724
0
    } else if (wbuf_add(b, s, 1) != 0) {
725
0
      return -1;
726
0
    }
727
0
  }
728
0
  return wbuf_add(b, "\"", 1);
729
0
}
730
731
/* Copy the media type of header "name" (up to ';'/space) from a raw HTTP header
732
   block into out; out is "" if absent. */
733
static void http_header_value(const char *hdr, const char *name, char *out,
734
0
                              size_t outsz) {
735
0
  size_t nl = strlen(name);
736
0
  const char *p = hdr;
737
0
  out[0] = '\0';
738
0
  if (hdr == NULL)
739
0
    return;
740
0
  while (*p != '\0') {
741
0
    const char *eol = strchr(p, '\n');
742
0
    size_t len = (eol != NULL) ? (size_t) (eol - p) : strlen(p);
743
0
    if (len > 0 && p[len - 1] == '\r')
744
0
      len--;
745
0
    if (len == 0)
746
0
      break; /* end of headers */
747
0
    if (len > nl && strncasecmp(p, name, nl) == 0 && p[nl] == ':') {
748
0
      const char *v = p + nl + 1;
749
0
      size_t vlen, k;
750
0
      while (v < p + len && (*v == ' ' || *v == '\t'))
751
0
        v++;
752
0
      vlen = (size_t) (p + len - v);
753
0
      for (k = 0; k < vlen; k++)
754
0
        if (v[k] == ';' || v[k] == ' ' || v[k] == '\t') {
755
0
          vlen = k;
756
0
          break;
757
0
        }
758
0
      if (vlen >= outsz)
759
0
        vlen = outsz - 1;
760
0
      memcpy(out, v, vlen);
761
0
      out[vlen] = '\0';
762
0
      return;
763
0
    }
764
0
    if (eol == NULL)
765
0
      break;
766
0
    p = eol + 1;
767
0
  }
768
0
}
769
770
/* Take ownership of a CDXJ line; frees it and returns -1 on OOM. */
771
0
static int cdx_lines_add(warc_writer *w, char *line) {
772
0
  if (w->cdx_count == w->cdx_cap) {
773
0
    size_t ncap = w->cdx_cap ? w->cdx_cap * 2 : 64;
774
0
    char **n;
775
0
    if (ncap > (size_t) -1 / sizeof(char *)) {
776
0
      freet(line);
777
0
      return -1;
778
0
    }
779
0
    n = realloct(w->cdx_lines, ncap * sizeof(char *));
780
0
    if (n == NULL) {
781
0
      freet(line);
782
0
      return -1;
783
0
    }
784
0
    w->cdx_lines = n;
785
0
    w->cdx_cap = ncap;
786
0
  }
787
0
  w->cdx_lines[w->cdx_count++] = line;
788
0
  return 0;
789
0
}
790
791
/* PRIu64 straight off the uint64_t: an `(unsigned long)` cast compiles clean
792
   and truncates past 4GB on LLP64 Windows, where only that leg's run of
793
   -#test=warc-offset would ever see the wrong offset. */
794
0
int warc_cdx_extent(char *out, size_t outsz, uint64_t length, uint64_t offset) {
795
0
  const int n = snprintf(
796
0
      out, outsz, ", \"length\": \"%" PRIu64 "\", \"offset\": \"%" PRIu64 "\"",
797
0
      length, offset);
798
799
0
  return (n < 0 || (size_t) n >= outsz) ? -1 : n;
800
0
}
801
802
/* Build and stash one CDXJ line for a record. Best-effort: an OOM drops the
803
   line rather than failing the (already-written) record. */
804
static void warc_cdx_add(warc_writer *w, const char *target_uri,
805
                         const char *date_iso, const char *status,
806
                         const char *mime, const char *payload_digest,
807
0
                         uint64_t offset, uint64_t length) {
808
0
  wbuf line;
809
0
  char ts[15];
810
0
  memset(&line, 0, sizeof(line));
811
0
  iso8601_to_cdx14(date_iso, ts);
812
0
  if (surt_canon(target_uri, &line) != 0 ||
813
0
      wbuf_printf(&line, " %s {\"url\": ", ts) != 0 ||
814
0
      cdx_json_str(&line, target_uri) != 0)
815
0
    goto fail;
816
0
  if (mime != NULL && mime[0] != '\0' &&
817
0
      (wbuf_puts(&line, ", \"mime\": ") != 0 || cdx_json_str(&line, mime) != 0))
818
0
    goto fail;
819
0
  if (status != NULL && status[0] != '\0' &&
820
0
      (wbuf_puts(&line, ", \"status\": ") != 0 ||
821
0
       cdx_json_str(&line, status) != 0))
822
0
    goto fail;
823
0
  if (payload_digest != NULL && payload_digest[0] != '\0' &&
824
0
      wbuf_printf(&line, ", \"digest\": \"sha1:%s\"", payload_digest) != 0)
825
0
    goto fail;
826
0
  {
827
0
    char extent[WARC_CDX_EXTENT_SIZE];
828
829
0
    if (warc_cdx_extent(extent, sizeof(extent), length, offset) < 0 ||
830
0
        wbuf_puts(&line, extent) != 0)
831
0
      goto fail;
832
0
  }
833
0
  if (w->cur_seg != NULL && (wbuf_puts(&line, ", \"filename\": ") != 0 ||
834
0
                             cdx_json_str(&line, w->cur_seg) != 0))
835
0
    goto fail;
836
0
  if (wbuf_puts(&line, "}") != 0 || wbuf_add(&line, "", 1) != 0) /* NUL */
837
0
    goto fail;
838
0
  if (cdx_lines_add(w, line.data) == 0)
839
0
    return; /* ownership transferred */
840
0
  return;   /* cdx_lines_add already freed on failure */
841
0
fail:
842
0
  wbuf_free(&line);
843
0
}
844
845
/* LC_ALL=C (unsigned byte) order over whole lines; the searchable key
846
   "<surt> <ts>" is the line prefix, so this yields sorted CDXJ. */
847
0
static int cdx_cmp(const void *a, const void *b) {
848
0
  const unsigned char *x = *(const unsigned char *const *) a;
849
0
  const unsigned char *y = *(const unsigned char *const *) b;
850
0
  while (*x != '\0' && *x == *y) {
851
0
    x++;
852
0
    y++;
853
0
  }
854
0
  return (int) *x - (int) *y;
855
0
}
856
857
/* Sort and write the accumulated CDXJ lines to <base>.cdx. */
858
0
static void warc_cdx_flush(warc_writer *w) {
859
0
  FILE *f;
860
0
  char catbuff[CATBUFF_SIZE];
861
0
  size_t i;
862
0
  int werr, cerr;
863
0
  if (!w->cdx_on || w->cdx_path == NULL)
864
0
    return;
865
0
  if (w->cdx_count == 0) {
866
    /* Stale only if an index is still on disk and this run wrote an archive
867
       over what it describes: opened covers the swap and the in-place run. */
868
0
    if (w->opened && fsize_utf8(w->cdx_path) > 0)
869
0
      hts_log_print(w->opt, LOG_ERROR,
870
0
                    "WARC: no record was indexed, %s was not rewritten",
871
0
                    w->cdx_path);
872
0
    return;
873
0
  }
874
0
  qsort(w->cdx_lines, w->cdx_count, sizeof(char *), cdx_cmp);
875
0
  f = FOPEN(fconv(catbuff, sizeof(catbuff), w->cdx_path), "wb");
876
0
  if (f == NULL) {
877
0
    hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
878
0
                  "WARC: could not write the index %s", w->cdx_path);
879
0
    return;
880
0
  }
881
0
  for (i = 0; i < w->cdx_count; i++) {
882
0
    fputs(w->cdx_lines[i], f);
883
0
    fputc('\n', f);
884
0
  }
885
0
  werr = ferror(f) != 0;
886
0
  cerr = fclose(f) != 0;
887
  /* a write that already failed says more than fclose echoing it, and only
888
     fclose's errno is still fresh */
889
0
  if (werr)
890
0
    hts_log_print(w->opt, LOG_ERROR, "WARC: the index %s is incomplete",
891
0
                  w->cdx_path);
892
0
  else if (cerr)
893
0
    hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
894
0
                  "WARC: could not write the index %s", w->cdx_path);
895
0
}
896
897
/* ---- WACZ pages + packaging (--wacz) ---- */
898
899
/* Record one pages.jsonl line for a top-level 200 text/html capture. First
900
   page also seeds datapackage mainPageUrl/mainPageDate. Best-effort. */
901
0
static void warc_page_add(warc_writer *w, const char *url, const char *date) {
902
0
  wbuf line;
903
0
  memset(&line, 0, sizeof(line));
904
0
  if (wbuf_printf(&line, "{\"id\": \"p%llu\", \"url\": ",
905
0
                  (unsigned long long) w->page_count) != 0 ||
906
0
      cdx_json_str(&line, url) != 0 || wbuf_puts(&line, ", \"ts\": ") != 0 ||
907
0
      cdx_json_str(&line, date) != 0 || wbuf_puts(&line, "}") != 0 ||
908
0
      wbuf_add(&line, "", 1) != 0) {
909
0
    wbuf_free(&line);
910
0
    return;
911
0
  }
912
0
  if (w->page_count == w->page_cap) {
913
0
    size_t ncap = w->page_cap ? w->page_cap * 2 : 32;
914
0
    char **n;
915
0
    if (ncap > (size_t) -1 / sizeof(char *)) {
916
0
      wbuf_free(&line);
917
0
      return;
918
0
    }
919
0
    n = realloct(w->page_lines, ncap * sizeof(char *));
920
0
    if (n == NULL) {
921
0
      wbuf_free(&line);
922
0
      return;
923
0
    }
924
0
    w->page_lines = n;
925
0
    w->page_cap = ncap;
926
0
  }
927
0
  w->page_lines[w->page_count++] = line.data;
928
0
  if (w->main_url == NULL) {
929
0
    w->main_url = strdupt(url);
930
0
    w->main_date = strdupt(date);
931
0
  }
932
0
}
933
934
#if HTS_USEOPENSSL
935
/* WACZ requires every ZIP entry stored, not deflated (spec 1.1.1). */
936
static int wacz_open_store(zipFile zf, const char *name) {
937
  zip_fileinfo zi;
938
  memset(&zi, 0, sizeof(zi));
939
  return zipOpenNewFileInZip(zf, name, &zi, NULL, 0, NULL, 0, NULL, 0 /*store*/,
940
                             0 /*level*/);
941
}
942
943
static int wacz_write_bytes(zipFile zf, const void *p, size_t n) {
944
  while (n > 0) {
945
    unsigned chunk = (n > 0x40000000u) ? 0x40000000u : (unsigned) n;
946
    if (zipWriteInFileInZip(zf, p, chunk) != ZIP_OK)
947
      return -1;
948
    p = (const char *) p + chunk;
949
    n -= chunk;
950
  }
951
  return 0;
952
}
953
954
/* Append one datapackage resource object; comma-prefixed unless first. */
955
static int wacz_resource_add(wbuf *res, int first, const char *name,
956
                             const char *path, const char *hex,
957
                             uint64_t bytes) {
958
  if (!first && wbuf_puts(res, ", ") != 0)
959
    return -1;
960
  if (wbuf_puts(res, "{\"name\": ") != 0 || cdx_json_str(res, name) != 0 ||
961
      wbuf_puts(res, ", \"path\": ") != 0 || cdx_json_str(res, path) != 0 ||
962
      wbuf_printf(res, ", \"hash\": \"sha256:%s\", \"bytes\": %llu}", hex,
963
                  (unsigned long long) bytes) != 0)
964
    return -1;
965
  return 0;
966
}
967
968
/* Store an on-disk file as zipname, hash it, and list it in res. */
969
static int wacz_add_disk(zipFile zf, const char *zipname, const char *diskpath,
970
                         const char *resname, wbuf *res, int first) {
971
  char hex[65];
972
  char catbuff[CATBUFF_SIZE];
973
  FILE *fp;
974
  uint64_t bytes = 0;
975
  int rc = -1;
976
  if (!sha256_hex_file(diskpath, hex))
977
    return -1;
978
  if (wacz_open_store(zf, zipname) != ZIP_OK)
979
    return -1;
980
  fp = FOPEN(fconv(catbuff, sizeof(catbuff), diskpath), "rb");
981
  if (fp != NULL) {
982
    rc = 0;
983
    for (;;) {
984
      char b[32768];
985
      size_t nl = fread(b, 1, sizeof(b), fp);
986
      if (nl > 0 && wacz_write_bytes(zf, b, nl) != 0) {
987
        rc = -1;
988
        break;
989
      }
990
      bytes += nl;
991
      if (nl < sizeof(b)) {
992
        if (ferror(fp))
993
          rc = -1;
994
        break;
995
      }
996
    }
997
    fclose(fp);
998
  }
999
  if (zipCloseFileInZip(zf) != ZIP_OK)
1000
    rc = -1;
1001
  if (rc == 0)
1002
    rc = wacz_resource_add(res, first, resname, zipname, hex, bytes);
1003
  return rc;
1004
}
1005
1006
/* Store in-memory bytes as zipname; when res != NULL, hash and list them. */
1007
static int wacz_add_mem(zipFile zf, const char *zipname, const void *data,
1008
                        size_t n, const char *resname, wbuf *res, int first) {
1009
  char hex[65];
1010
  if (res != NULL && !sha256_hex_mem(data, n, hex))
1011
    return -1;
1012
  if (wacz_open_store(zf, zipname) != ZIP_OK)
1013
    return -1;
1014
  if (wacz_write_bytes(zf, data, n) != 0) {
1015
    zipCloseFileInZip(zf);
1016
    return -1;
1017
  }
1018
  if (zipCloseFileInZip(zf) != ZIP_OK)
1019
    return -1;
1020
  if (res != NULL)
1021
    return wacz_resource_add(res, first, resname, zipname, hex, n);
1022
  return 0;
1023
}
1024
1025
/* Package the segment(s) + .cdx + a generated pages.jsonl into <base>.wacz at
1026
   crawl end (the archive file(s) and .cdx are already closed on disk). */
1027
static void warc_wacz_package(warc_writer *w) {
1028
  char waczpath[HTS_URLMAXSIZE * 2];
1029
  char tmppath[HTS_URLMAXSIZE * 2];
1030
  char segpath[HTS_URLMAXSIZE * 2];
1031
  char catbuff[CATBUFF_SIZE];
1032
  zipFile zf;
1033
  wbuf pages, resources, dp, digest;
1034
  char *seg_name;
1035
  char dp_hex[65];
1036
  char created[32];
1037
  unsigned s, nseg;
1038
  int err = 0, first = 1;
1039
  size_t i;
1040
1041
  if (w->base_path == NULL || w->cdx_path == NULL)
1042
    return;
1043
  snprintf(waczpath, sizeof(waczpath), "%s.wacz", w->base_path);
1044
  snprintf(tmppath, sizeof(tmppath), "%s.wacz.tmp", w->base_path);
1045
  /* Build into a temp; only full success replaces <base>.wacz, so a zero-record
1046
     re-run can't destroy a good archive (#522). */
1047
  zf = hts_zipOpen_utf8(fconv(catbuff, sizeof(catbuff), tmppath), 0);
1048
  if (zf == NULL) {
1049
    hts_log_print(w->opt, LOG_WARNING, "WACZ: could not create %s", tmppath);
1050
    return;
1051
  }
1052
  memset(&resources, 0, sizeof(resources));
1053
1054
  /* archive/<name>.warc.gz for every segment (single file, or 0..seg). */
1055
  nseg = (w->max_size > 0) ? w->seg + 1 : 1;
1056
  for (s = 0; s < nseg && !err; s++) {
1057
    char zipname[HTS_URLMAXSIZE];
1058
    if (w->max_size > 0)
1059
      snprintf(segpath, sizeof(segpath), "%s-%05u%s", w->base_path, s,
1060
               w->base_ext);
1061
    else
1062
      strlcpybuff(segpath, w->arc_path != NULL ? w->arc_path : w->base_path,
1063
                  sizeof(segpath));
1064
    seg_name = path_basename_dup(segpath);
1065
    if (seg_name == NULL) {
1066
      err = 1;
1067
      break;
1068
    }
1069
    snprintf(zipname, sizeof(zipname), "archive/%s", seg_name);
1070
    if (wacz_add_disk(zf, zipname, segpath, seg_name, &resources, first) != 0)
1071
      err = 1;
1072
    freet(seg_name);
1073
    first = 0;
1074
  }
1075
1076
  /* indexes/index.cdx */
1077
  if (!err && wacz_add_disk(zf, "indexes/index.cdx", w->cdx_path, "index.cdx",
1078
                            &resources, first) != 0)
1079
    err = 1;
1080
  first = 0;
1081
1082
  /* pages/pages.jsonl: header line + one line per captured page. */
1083
  memset(&pages, 0, sizeof(pages));
1084
  if (!err &&
1085
      wbuf_puts(&pages, "{\"format\": \"json-pages-1.0\", \"id\": \"pages\", "
1086
                        "\"title\": \"All Pages\"}\n") != 0)
1087
    err = 1;
1088
  for (i = 0; i < w->page_count && !err; i++)
1089
    if (wbuf_puts(&pages, w->page_lines[i]) != 0 ||
1090
        wbuf_add(&pages, "\n", 1) != 0)
1091
      err = 1;
1092
  if (!err && wacz_add_mem(zf, "pages/pages.jsonl", pages.data, pages.len,
1093
                           "pages.jsonl", &resources, first) != 0)
1094
    err = 1;
1095
  wbuf_free(&pages);
1096
1097
  /* datapackage.json listing every stored file with its sha256 + size. */
1098
  hts_now_iso8601(created);
1099
  memset(&dp, 0, sizeof(dp));
1100
  if (!err &&
1101
      (wbuf_printf(&dp,
1102
                   "{\"profile\": \"data-package\", \"wacz_version\": "
1103
                   "\"1.1.1\", \"software\": \"HTTrack/%s\", \"created\": ",
1104
                   HTTRACK_VERSION) != 0 ||
1105
       cdx_json_str(&dp, created) != 0))
1106
    err = 1;
1107
  if (!err && w->main_url != NULL &&
1108
      (wbuf_puts(&dp, ", \"mainPageUrl\": ") != 0 ||
1109
       cdx_json_str(&dp, w->main_url) != 0 ||
1110
       wbuf_puts(&dp, ", \"mainPageDate\": ") != 0 ||
1111
       cdx_json_str(&dp, w->main_date != NULL ? w->main_date : created) != 0))
1112
    err = 1;
1113
  if (!err && (wbuf_puts(&dp, ", \"resources\": [") != 0 ||
1114
               wbuf_add(&dp, resources.data, resources.len) != 0 ||
1115
               wbuf_puts(&dp, "]}") != 0))
1116
    err = 1;
1117
  if (!err &&
1118
      wacz_add_mem(zf, "datapackage.json", dp.data, dp.len, NULL, NULL, 0) != 0)
1119
    err = 1;
1120
1121
  /* datapackage-digest.json chains the integrity of datapackage.json. */
1122
  memset(&digest, 0, sizeof(digest));
1123
  if (!err && sha256_hex_mem(dp.data, dp.len, dp_hex)) {
1124
    if (wbuf_printf(&digest,
1125
                    "{\"path\": \"datapackage.json\", \"hash\": \"sha256:%s\"}",
1126
                    dp_hex) != 0 ||
1127
        wacz_add_mem(zf, "datapackage-digest.json", digest.data, digest.len,
1128
                     NULL, NULL, 0) != 0)
1129
      err = 1;
1130
  } else {
1131
    err = 1;
1132
  }
1133
  wbuf_free(&digest);
1134
  wbuf_free(&dp);
1135
  wbuf_free(&resources);
1136
1137
  zipClose(zf, NULL);
1138
  if (err) {
1139
    (void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
1140
    hts_log_print(w->opt, LOG_WARNING,
1141
                  "WACZ: packaging failed, kept existing %s untouched",
1142
                  waczpath);
1143
  } else if (!hts_rename_over(w->opt, tmppath, waczpath)) {
1144
    (void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
1145
    hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
1146
                  "WACZ: could not finalize %s", waczpath);
1147
  }
1148
}
1149
#endif
1150
1151
/* Close the current segment and open the next; writes its warcinfo. */
1152
static int warc_rotate(warc_writer *w);
1153
1154
/* Emit one full WARC record. When http_section, the block carries an HTTP
1155
   header block (http_hdr, possibly empty) + a CRLF separator; body follows when
1156
   has_body. block_len is derived here (single source of truth: separator and
1157
   payload are counted exactly as stream_block emits them), so a declared
1158
   Content-Length can never desync from the written bytes. The payload digest
1159
   (body-only) is passed in when already known. truncated is a WARC-Truncated
1160
   reason token or NULL. On success the record id is copied to out_id (may be
1161
   NULL). */
1162
static int warc_emit(warc_writer *w, const char *type, const char *content_type,
1163
                     const char *target_uri, const char *ip,
1164
                     const char *concurrent_to, const char *refers_uri,
1165
                     const char *refers_date, const char *profile,
1166
                     const char *payload_digest, const char *truncated,
1167
                     const char *cdx_status, const char *cdx_mime,
1168
                     int http_section, const char *http_hdr,
1169
                     size_t http_hdr_len, int has_body, const char *body,
1170
0
                     size_t body_len, const char *body_path, char out_id[64]) {
1171
0
  wbuf hdr;
1172
0
  member m;
1173
0
  char id[64], date[32];
1174
0
  size_t sep = http_section ? 2 : 0;
1175
0
  size_t payload = has_body ? body_len : 0;
1176
0
  size_t block_len;
1177
0
  int rc = -1;
1178
#if HTS_USEOPENSSL
1179
  digester d;
1180
  unsigned char md[EVP_MAX_MD_SIZE];
1181
  unsigned int mdlen = 0;
1182
  char block_b32[33];
1183
  int have_block_digest = 0;
1184
#endif
1185
1186
  /* Rotate to the next segment before this record when the current one is full;
1187
     never split a record, and never rotate a warcinfo (it opens a segment). */
1188
0
  if (w->max_size > 0 && w->seg_base != NULL && w->offset >= w->max_size &&
1189
0
      strcmp(type, "warcinfo") != 0) {
1190
0
    if (warc_rotate(w) != 0) {
1191
0
      w->failed = HTS_TRUE;
1192
0
      return -1;
1193
0
    }
1194
0
  }
1195
1196
  /* F4: overflow-safe block length; http_hdr_len+sep is provably small. */
1197
0
  if (payload > (size_t) -1 - http_hdr_len - sep) {
1198
0
    w->failed = HTS_TRUE;
1199
0
    return -1;
1200
0
  }
1201
0
  block_len = http_hdr_len + sep + payload;
1202
1203
0
  memset(&hdr, 0, sizeof(hdr));
1204
0
  warc_make_id(w, id);
1205
0
  hts_now_iso8601(date);
1206
1207
#if HTS_USEOPENSSL
1208
  /* Block digest over the whole block, in one streaming pass. */
1209
  d.block = EVP_MD_CTX_new();
1210
  d.payload = NULL;
1211
  if (d.block != NULL && EVP_DigestInit_ex(d.block, EVP_sha1(), NULL) == 1 &&
1212
      stream_block(http_section, http_hdr, http_hdr_len, has_body, body,
1213
                   body_len, body_path, digest_sink, &d) == 0 &&
1214
      EVP_DigestFinal_ex(d.block, md, &mdlen) == 1 && mdlen == 20) {
1215
    base32_20(md, block_b32);
1216
    have_block_digest = 1;
1217
  }
1218
  if (d.block != NULL)
1219
    EVP_MD_CTX_free(d.block);
1220
#endif
1221
1222
0
  if (wbuf_puts(&hdr, "WARC/1.1\r\n") != 0 ||
1223
0
      wbuf_printf(&hdr, "WARC-Type: %s\r\n", type) != 0 ||
1224
0
      wbuf_printf(&hdr, "WARC-Record-ID: %s\r\n", id) != 0 ||
1225
0
      wbuf_printf(&hdr, "WARC-Date: %s\r\n", date) != 0)
1226
0
    goto done;
1227
0
  if (content_type != NULL &&
1228
0
      wbuf_printf(&hdr, "Content-Type: %s\r\n", content_type) != 0)
1229
0
    goto done;
1230
0
  if (wbuf_printf(&hdr, "Content-Length: %llu\r\n",
1231
0
                  (unsigned long long) block_len) != 0)
1232
0
    goto done;
1233
0
  if (w->info_id[0] != '\0' && strcmp(type, "warcinfo") != 0 &&
1234
0
      wbuf_printf(&hdr, "WARC-Warcinfo-ID: %s\r\n", w->info_id) != 0)
1235
0
    goto done;
1236
0
  if (target_uri != NULL && target_uri[0] != '\0' &&
1237
0
      wbuf_printf(&hdr, "WARC-Target-URI: %s\r\n", target_uri) != 0)
1238
0
    goto done;
1239
0
  if (ip != NULL && ip[0] != '\0' &&
1240
0
      wbuf_printf(&hdr, "WARC-IP-Address: %s\r\n", ip) != 0)
1241
0
    goto done;
1242
0
  if (concurrent_to != NULL && concurrent_to[0] != '\0' &&
1243
0
      wbuf_printf(&hdr, "WARC-Concurrent-To: %s\r\n", concurrent_to) != 0)
1244
0
    goto done;
1245
0
  if (profile != NULL &&
1246
0
      wbuf_printf(&hdr, "WARC-Profile: %s\r\n", profile) != 0)
1247
0
    goto done;
1248
0
  if (refers_uri != NULL && refers_uri[0] != '\0' &&
1249
0
      wbuf_printf(&hdr, "WARC-Refers-To-Target-URI: %s\r\n", refers_uri) != 0)
1250
0
    goto done;
1251
0
  if (refers_date != NULL && refers_date[0] != '\0' &&
1252
0
      wbuf_printf(&hdr, "WARC-Refers-To-Date: %s\r\n", refers_date) != 0)
1253
0
    goto done;
1254
#if HTS_USEOPENSSL
1255
  if (have_block_digest &&
1256
      wbuf_printf(&hdr, "WARC-Block-Digest: sha1:%s\r\n", block_b32) != 0)
1257
    goto done;
1258
#endif
1259
0
  if (payload_digest != NULL && payload_digest[0] != '\0' &&
1260
0
      wbuf_printf(&hdr, "WARC-Payload-Digest: sha1:%s\r\n", payload_digest) !=
1261
0
          0)
1262
0
    goto done;
1263
0
  if (truncated != NULL &&
1264
0
      wbuf_printf(&hdr, "WARC-Truncated: %s\r\n", truncated) != 0)
1265
0
    goto done;
1266
0
  if (wbuf_puts(&hdr, "\r\n") != 0)
1267
0
    goto done;
1268
1269
0
  if (member_begin(&m, w) != 0)
1270
0
    goto done;
1271
0
  { /* member start (before writing): CDXJ offset for this record */
1272
0
    uint64_t rec_offset = w->offset;
1273
0
    if (member_write(&m, hdr.data, hdr.len) != 0 ||
1274
0
        stream_block(http_section, http_hdr, http_hdr_len, has_body, body,
1275
0
                     body_len, body_path, write_sink, &m) != 0 ||
1276
0
        member_write(&m, "\r\n\r\n", 4) != 0) {
1277
0
      member_end(&m);
1278
0
      goto done;
1279
0
    }
1280
0
    if (member_end(&m) != 0)
1281
0
      goto done;
1282
1283
0
    w->offset = warc_stream_offset(w->f, w->offset);
1284
    /* Index response/revisit/resource records only (not warcinfo/request). */
1285
0
    if (w->cdx_on && target_uri != NULL && target_uri[0] != '\0' &&
1286
0
        (strcmp(type, "response") == 0 || strcmp(type, "revisit") == 0 ||
1287
0
         strcmp(type, "resource") == 0))
1288
0
      warc_cdx_add(w, target_uri, date, cdx_status, cdx_mime, payload_digest,
1289
0
                   rec_offset, w->offset - rec_offset);
1290
    /* WACZ pages: top-level 200 text/html captures. */
1291
0
    if (w->wacz_on && target_uri != NULL && target_uri[0] != '\0' &&
1292
0
        strcmp(type, "response") == 0 && cdx_status != NULL &&
1293
0
        strcmp(cdx_status, "200") == 0 && cdx_mime != NULL &&
1294
0
        strncasecmp(cdx_mime, "text/html", 9) == 0)
1295
0
      warc_page_add(w, target_uri, date);
1296
0
  }
1297
0
  if (out_id != NULL)
1298
0
    strlcpybuff(out_id, id, 64);
1299
0
  rc = 0;
1300
0
done:
1301
0
  wbuf_free(&hdr);
1302
0
  if (rc != 0)
1303
0
    w->failed = HTS_TRUE; /* a truncated run must not replace a whole one */
1304
0
  return rc;
1305
0
}
1306
1307
/* Byte offset of f, for the CDXJ index and the segment-rotation cap. ftello,
1308
   never ftell: long tops out at 2GB, and is 32-bit even on 64-bit Windows, so
1309
   every offset past the cap would be indexed wrong. `current` on failure. */
1310
0
uint64_t warc_stream_offset(FILE *f, uint64_t current) {
1311
0
  const LLint pos = ftello(f);
1312
1313
  /* ftell/long would compile here and cap at 2GB on LLP64 and 32-bit hosts */
1314
0
  HTS_STATIC_ASSERT(sizeof(pos) >= 8 && sizeof(ftello(f)) >= 8,
1315
0
                    warc_tell_is_64bit);
1316
0
  return pos >= 0 ? (uint64_t) pos : current;
1317
0
}
1318
1319
/* ---- segment rotation (--warc-max-size) ---- */
1320
1321
/* Path to open for the segment whose final path is `final`: that path itself,
1322
   or a sibling temp while a previous archive must survive until close. */
1323
static const char *warc_open_path(warc_writer *w, const char *final, char *buf,
1324
0
                                  size_t bufsz) {
1325
0
  if (!w->protect_prev)
1326
0
    return final;
1327
0
  snprintf(buf, bufsz, "%s" WARC_TMP_SUFFIX, final);
1328
0
  return buf;
1329
0
}
1330
1331
/* Emit the warcinfo that heads a segment; sets w->info_id for its records. */
1332
0
static int warc_write_warcinfo_record(warc_writer *w) {
1333
0
  w->info_id[0] = '\0'; /* warcinfo itself carries no WARC-Warcinfo-ID */
1334
0
  return warc_emit(
1335
0
      w, "warcinfo", "application/warc-fields", NULL, NULL, NULL, NULL, NULL,
1336
0
      NULL, NULL, NULL, NULL, NULL, 0, NULL, 0, 1, w->info_fields,
1337
0
      w->info_fields != NULL ? strlen(w->info_fields) : 0, NULL, w->info_id);
1338
0
}
1339
1340
0
static int warc_rotate(warc_writer *w) {
1341
0
  char namebuf[HTS_URLMAXSIZE * 2];
1342
0
  char openbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
1343
0
  char catbuff[CATBUFF_SIZE];
1344
0
  const unsigned next = w->seg + 1;
1345
0
  FILE *f;
1346
0
  snprintf(namebuf, sizeof(namebuf), "%s-%05u%s", w->seg_base, next,
1347
0
           w->seg_ext);
1348
  /* Open before advancing: w->seg must only ever name a segment that exists,
1349
     or close-time packaging and swapping would work on a missing file. */
1350
0
  f = FOPEN(fconv(catbuff, sizeof(catbuff),
1351
0
                  warc_open_path(w, namebuf, openbuf, sizeof(openbuf))),
1352
0
            "wb");
1353
0
  if (f == NULL)
1354
0
    return -1;
1355
0
  if (w->f != NULL)
1356
0
    fclose(w->f);
1357
0
  w->f = f;
1358
0
  w->seg = next;
1359
0
  w->offset = 0;
1360
0
  if (w->cdx_on) {
1361
0
    freet(w->cur_seg);
1362
0
    w->cur_seg = path_basename_dup(namebuf);
1363
0
  }
1364
0
  return warc_write_warcinfo_record(w);
1365
0
}
1366
1367
/* ---- request stash (engine hooks) ---- */
1368
1369
0
void warc_stash_request(htsblk *r, const char *reqhdr) {
1370
0
  if (r == NULL)
1371
0
    return;
1372
0
  freet(r->warc_reqhdr);
1373
0
  if (reqhdr != NULL)
1374
0
    r->warc_reqhdr = strdupt(reqhdr);
1375
0
}
1376
1377
0
void warc_stash_response(htsblk *r, const char *resphdr) {
1378
0
  if (r == NULL)
1379
0
    return;
1380
0
  freet(r->warc_resphdr);
1381
0
  if (resphdr != NULL)
1382
0
    r->warc_resphdr = strdupt(resphdr);
1383
0
}
1384
1385
0
void warc_free_request(htsblk *r) {
1386
0
  if (r != NULL) {
1387
0
    freet(r->warc_reqhdr);
1388
0
    freet(r->warc_resphdr);
1389
0
    if (r->warc_rawpath != NULL) {
1390
0
      (void) UNLINK(r->warc_rawpath); /* owns the verbatim spool file */
1391
0
      back_tmpdir_drop(r->warc_rawpath);
1392
0
      freet(r->warc_rawpath);
1393
0
      r->warc_rawpath = NULL;
1394
0
    }
1395
0
  }
1396
0
}
1397
1398
0
void warc_move_request(htsblk *src, htsblk *dst) {
1399
0
  if (src == NULL || dst == NULL || src == dst)
1400
0
    return;
1401
0
  freet(dst->warc_reqhdr);
1402
0
  freet(dst->warc_resphdr);
1403
0
  dst->warc_reqhdr = src->warc_reqhdr;
1404
0
  dst->warc_resphdr = src->warc_resphdr;
1405
0
  src->warc_reqhdr = NULL;
1406
0
  src->warc_resphdr = NULL;
1407
0
}
1408
1409
0
void warc_adopt_rawspool(htsblk *r, const char *tmpfile_path) {
1410
0
  if (r != NULL) {
1411
0
    LLint rawsize;
1412
0
    freet(r->warc_rawpath);
1413
0
    r->warc_rawpath = NULL;
1414
0
    r->warc_rawsize = 0;
1415
0
    if (strnotempty(tmpfile_path) && (rawsize = fsize_utf8(tmpfile_path)) > 0 &&
1416
0
        (r->warc_rawpath = strdupt(tmpfile_path)) != NULL) {
1417
0
      r->warc_rawsize = rawsize;
1418
0
    }
1419
0
  }
1420
0
}
1421
1422
/* ---- open / close ---- */
1423
1424
0
warc_writer *warc_open(httrackp *opt, const char *path) {
1425
0
  warc_writer *w;
1426
0
  char namebuf[HTS_URLMAXSIZE * 2];
1427
0
  char openbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
1428
0
  char catbuff[CATBUFF_SIZE];
1429
0
  wbuf info;
1430
0
  const char *robots;
1431
0
  size_t plen;
1432
1433
0
  if (path == NULL)
1434
0
    return NULL;
1435
1436
  /* --warc with no name: <output>/httrack-<timestamp>.warc.gz */
1437
0
  if (strcmp(path, WARC_AUTONAME) == 0) {
1438
0
    char ts[32];
1439
0
    time_t t = time(NULL);
1440
0
    struct tm tmv;
1441
0
    if (!hts_gmtime(t, &tmv))
1442
0
      memset(&tmv, 0, sizeof(tmv));
1443
0
    strftime(ts, sizeof(ts), "%Y%m%d%H%M%S", &tmv);
1444
0
    snprintf(catbuff, sizeof(catbuff), "httrack-%s.warc.gz", ts);
1445
0
    path =
1446
0
        fconcat(namebuf, sizeof(namebuf), StringBuff(opt->path_html), catbuff);
1447
0
  } else {
1448
    /* --warc-file NAME: append .warc.gz unless already a .warc/.warc.gz name;
1449
       place bare basenames under the output directory (like the auto name). */
1450
0
    size_t l = strlen(path);
1451
0
    int has_warc = (l >= 5 && strcasecmp(path + l - 5, ".warc") == 0);
1452
0
    int has_gz = (l >= 3 && strcasecmp(path + l - 3, ".gz") == 0);
1453
0
    char named[HTS_URLMAXSIZE];
1454
0
    if (has_warc || has_gz)
1455
0
      strlcpybuff(named, path, sizeof(named));
1456
0
    else
1457
0
      snprintf(named, sizeof(named), "%s.warc.gz", path);
1458
0
    if (strchr(named, '/') == NULL && strchr(named, '\\') == NULL) {
1459
0
      path =
1460
0
          fconcat(namebuf, sizeof(namebuf), StringBuff(opt->path_html), named);
1461
0
    } else {
1462
0
      strlcpybuff(namebuf, named, sizeof(namebuf));
1463
0
      path = namebuf;
1464
0
    }
1465
0
  }
1466
1467
0
  w = calloct(1, sizeof(*w));
1468
0
  if (w == NULL)
1469
0
    return NULL;
1470
0
  w->opt = opt;
1471
0
  plen = strlen(path);
1472
0
  w->gz = (plen >= 3 && strcasecmp(path + plen - 3, ".gz") == 0);
1473
0
  w->rng = (uint64_t) time(NULL) ^ ((uint64_t) (uintptr_t) w << 16) ^
1474
0
           0x9e3779b97f4a7c15ULL;
1475
0
  w->seen = coucal_new(0);
1476
0
  if (w->seen != NULL)
1477
0
    coucal_value_is_malloc(w->seen, 1);
1478
0
  w->max_size = (opt->warc_max_size > 0) ? (uint64_t) opt->warc_max_size : 0;
1479
1480
  /* Build the warcinfo body once; each segment re-emits it. */
1481
0
  robots = (opt->robots == HTS_ROBOTS_NEVER) ? "ignore" : "obey";
1482
0
  memset(&info, 0, sizeof(info));
1483
0
  if (wbuf_printf(&info,
1484
0
                  "software: HTTrack/%s (+https://www.httrack.com/)\r\n"
1485
0
                  "format: WARC file version 1.1\r\n"
1486
0
                  "conformsTo: http://iipc.github.io/warc-specifications/"
1487
0
                  "specifications/warc-format/warc-1.1/\r\n"
1488
0
                  "robots: %s\r\n",
1489
0
                  HTTRACK_VERSION, robots) != 0 ||
1490
0
      (StringNotEmpty(opt->path_html) &&
1491
0
       wbuf_printf(&info, "isPartOf: %s\r\n", StringBuff(opt->path_html)) !=
1492
0
           0) ||
1493
0
      wbuf_add(&info, "", 1) != 0) { /* NUL-terminate for info_fields */
1494
0
    wbuf_free(&info);
1495
0
    warc_close(w);
1496
0
    return NULL;
1497
0
  }
1498
0
  w->info_fields = strdupt(info.data);
1499
0
  wbuf_free(&info);
1500
0
  if (w->info_fields == NULL) {
1501
0
    warc_close(w);
1502
0
    return NULL;
1503
0
  }
1504
1505
  /* --warc-cdx: <base>.cdx next to the resolved archive path (pre-rotation). */
1506
0
  if (opt->warc_cdx) {
1507
0
    size_t l = strlen(path);
1508
0
    size_t baselen = l;
1509
0
    if (l >= 8 && strcasecmp(path + l - 8, ".warc.gz") == 0) {
1510
0
      baselen = l - 8;
1511
0
      w->base_ext = ".warc.gz";
1512
0
    } else if (l >= 5 && strcasecmp(path + l - 5, ".warc") == 0) {
1513
0
      baselen = l - 5;
1514
0
      w->base_ext = ".warc";
1515
0
    } else {
1516
0
      w->base_ext = w->gz ? ".warc.gz" : ".warc";
1517
0
    }
1518
0
    w->cdx_on = 1;
1519
0
    w->cdx_path = malloct(baselen + 5); /* ".cdx" + NUL */
1520
0
    w->base_path = malloct(baselen + 1);
1521
0
    if (w->cdx_path == NULL || w->base_path == NULL) {
1522
0
      warc_close(w);
1523
0
      return NULL;
1524
0
    }
1525
0
    memcpy(w->cdx_path, path, baselen);
1526
0
    memcpy(w->cdx_path + baselen, ".cdx", 5);
1527
0
    memcpy(w->base_path, path, baselen);
1528
0
    w->base_path[baselen] = '\0';
1529
0
  }
1530
1531
  /* --wacz packages archive+cdx+pages at close; SHA-256 needs OpenSSL. */
1532
0
  if (opt->warc_wacz) {
1533
#if HTS_USEOPENSSL
1534
    w->wacz_on = 1;
1535
#else
1536
0
    hts_log_print(opt, LOG_WARNING,
1537
0
                  "WACZ requires an OpenSSL-enabled build for SHA-256 digests; "
1538
0
                  "--wacz disabled (WARC and CDXJ still written)");
1539
0
#endif
1540
0
  }
1541
1542
  /* Rotation on: the first segment is <base>-00000<ext> (wget-style); split the
1543
     resolved path into base + suffix so later segments reuse the base. */
1544
0
  if (w->max_size > 0) {
1545
0
    size_t l = strlen(path);
1546
0
    size_t baselen;
1547
0
    if (l >= 8 && strcasecmp(path + l - 8, ".warc.gz") == 0) {
1548
0
      baselen = l - 8;
1549
0
      w->seg_ext = ".warc.gz";
1550
0
    } else if (l >= 5 && strcasecmp(path + l - 5, ".warc") == 0) {
1551
0
      baselen = l - 5;
1552
0
      w->seg_ext = ".warc";
1553
0
    } else {
1554
0
      baselen = l;
1555
0
      w->seg_ext = w->gz ? ".warc.gz" : ".warc";
1556
0
    }
1557
0
    w->seg_base = malloct(baselen + 1);
1558
0
    if (w->seg_base == NULL) {
1559
0
      warc_close(w);
1560
0
      return NULL;
1561
0
    }
1562
0
    memcpy(w->seg_base, path, baselen);
1563
0
    w->seg_base[baselen] = '\0';
1564
0
    w->seg = 0;
1565
0
    snprintf(namebuf, sizeof(namebuf), "%s-%05u%s", w->seg_base, w->seg,
1566
0
             w->seg_ext);
1567
0
    path = namebuf;
1568
0
  }
1569
1570
0
  if (w->max_size == 0 && (w->arc_path = strdupt(path)) == NULL) {
1571
0
    warc_close(w);
1572
0
    return NULL;
1573
0
  }
1574
  /* Set only once arc_path is recorded, so a half-built writer never swaps. */
1575
0
  w->protect_prev = fsize_utf8(path) > 0 ? HTS_TRUE : HTS_FALSE;
1576
0
  w->f = FOPEN(fconv(catbuff, sizeof(catbuff),
1577
0
                     warc_open_path(w, path, openbuf, sizeof(openbuf))),
1578
0
               "wb");
1579
0
  if (w->f == NULL) {
1580
0
    warc_close(w);
1581
0
    return NULL;
1582
0
  }
1583
0
  if (w->cdx_on)
1584
0
    w->cur_seg = path_basename_dup(path);
1585
1586
0
  if (warc_write_warcinfo_record(w) != 0) {
1587
0
    warc_close(w);
1588
0
    return NULL;
1589
0
  }
1590
0
  w->opened = HTS_TRUE;
1591
0
  return w;
1592
0
}
1593
1594
/* Final path of segment s (the run's only archive when rotation is off). */
1595
static const char *warc_seg_path(warc_writer *w, unsigned s, char *buf,
1596
0
                                 size_t bufsz) {
1597
0
  if (w->max_size == 0)
1598
0
    return w->arc_path;
1599
0
  snprintf(buf, bufsz, "%s-%05u%s", w->seg_base, s, w->seg_ext);
1600
0
  return buf;
1601
0
}
1602
1603
/* Swap this run's archive into place, unless it only holds revisits naming
1604
   bodies the previous archive still has and this one does not (#759).
1605
   HTS_FALSE: the previous archive was kept, so leave its .cdx and .wacz too. */
1606
0
static hts_boolean warc_commit(warc_writer *w) {
1607
0
  char finalbuf[HTS_URLMAXSIZE * 2];
1608
0
  char tmpbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
1609
0
  char catbuff[CATBUFF_SIZE];
1610
0
  const unsigned nseg = (w->max_size > 0) ? w->seg + 1 : 1;
1611
0
  hts_boolean swap;
1612
0
  unsigned s;
1613
1614
0
  if (!w->protect_prev)
1615
0
    return HTS_TRUE; /* nothing was there to lose: written in place */
1616
1617
  /* All or nothing: a swap stopping halfway would mix this run's segments with
1618
     the previous one's, so require every temp before renaming any. */
1619
0
  swap = w->opened && !w->failed && w->unbacked_revisits == 0;
1620
0
  for (s = 0; s < nseg && swap; s++) {
1621
0
    snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX,
1622
0
             warc_seg_path(w, s, finalbuf, sizeof(finalbuf)));
1623
0
    swap = fsize_utf8(tmpbuf) > 0;
1624
0
  }
1625
1626
0
  if (swap) {
1627
0
    for (s = 0; s < nseg; s++) {
1628
0
      const char *final = warc_seg_path(w, s, finalbuf, sizeof(finalbuf));
1629
0
      snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX, final);
1630
0
      if (!hts_rename_over(w->opt, tmpbuf, final)) {
1631
0
        hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
1632
0
                      "WARC: could not replace %s", final);
1633
0
        return HTS_FALSE;
1634
0
      }
1635
0
    }
1636
0
    return HTS_TRUE;
1637
0
  }
1638
1639
0
  for (s = 0; s < nseg; s++) {
1640
0
    snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX,
1641
0
             warc_seg_path(w, s, finalbuf, sizeof(finalbuf)));
1642
0
    (void) UNLINK(fconv(catbuff, sizeof(catbuff), tmpbuf));
1643
0
  }
1644
0
  if (w->unbacked_revisits > 0)
1645
0
    hts_log_print(
1646
0
        w->opt, LOG_ERROR,
1647
0
        "WARC: this pass revisited %llu URL(s) without re-downloading them, so "
1648
0
        "this archive doesn't hold their current content; kept the previous %s "
1649
0
        "(re-run with -C0, or --warc-file with a name of its own)",
1650
0
        (unsigned long long) w->unbacked_revisits,
1651
0
        warc_seg_path(w, 0, finalbuf, sizeof(finalbuf)));
1652
0
  return HTS_FALSE;
1653
0
}
1654
1655
0
void warc_close(warc_writer *w) {
1656
0
  size_t i;
1657
0
  if (w == NULL)
1658
0
    return;
1659
0
  if (w->f != NULL)
1660
0
    fclose(w->f);
1661
0
  w->f = NULL;
1662
0
  if (warc_commit(w)) {
1663
0
    warc_cdx_flush(w); /* sort + write <base>.cdx beside the archive */
1664
#if HTS_USEOPENSSL
1665
    if (w->wacz_on) /* package once the segment(s) + .cdx are closed on disk */
1666
      warc_wacz_package(w);
1667
#endif
1668
0
  }
1669
0
  if (w->seen != NULL)
1670
0
    coucal_delete(&w->seen);
1671
0
  for (i = 0; i < w->cdx_count; i++)
1672
0
    freet(w->cdx_lines[i]);
1673
0
  freet(w->cdx_lines);
1674
0
  freet(w->cdx_path);
1675
0
  for (i = 0; i < w->page_count; i++)
1676
0
    freet(w->page_lines[i]);
1677
0
  freet(w->page_lines);
1678
0
  freet(w->base_path);
1679
0
  freet(w->arc_path);
1680
0
  freet(w->main_url);
1681
0
  freet(w->main_date);
1682
0
  freet(w->cur_seg);
1683
0
  freet(w->seg_base);
1684
0
  freet(w->info_fields);
1685
0
  freet(w);
1686
0
}
1687
1688
0
int warc_surt(const char *url, char *out, size_t outsz) {
1689
0
  wbuf b;
1690
0
  int rc = -1;
1691
0
  memset(&b, 0, sizeof(b));
1692
0
  if (surt_canon(url, &b) == 0 && wbuf_add(&b, "", 1) == 0 && b.len <= outsz) {
1693
0
    strlcpybuff(out, b.data, outsz);
1694
0
    rc = 0;
1695
0
  }
1696
0
  wbuf_free(&b);
1697
0
  return rc;
1698
0
}
1699
1700
0
void warc_close_opt(httrackp *opt) {
1701
0
  if (opt->state.warc != NULL && opt->state.warc != WARC_DISABLED) {
1702
0
    warc_close((warc_writer *) opt->state.warc);
1703
0
  }
1704
  /* final: teardown still finalizes slots, and a reopen there would orphan a
1705
     .tmp nobody closes (#1060) */
1706
0
  opt->state.warc = WARC_DISABLED;
1707
0
}
1708
1709
0
void warc_abort_opt(httrackp *opt) {
1710
0
  if (opt->state.warc != NULL && opt->state.warc != WARC_DISABLED) {
1711
0
    warc_writer *const w = (warc_writer *) opt->state.warc;
1712
    /* the session is rolling back, so this run must replace nothing */
1713
0
    w->failed = HTS_TRUE;
1714
0
    warc_close(w);
1715
0
  }
1716
0
  opt->state.warc = WARC_DISABLED;
1717
0
}
1718
1719
/* ---- one transaction ---- */
1720
1721
int warc_write_transaction(warc_writer *w, const char *target_uri,
1722
                           const char *ip, const char *req_hdr,
1723
                           const char *resp_hdr, const char *body,
1724
                           size_t body_len, const char *body_path,
1725
                           const char *content_type, int statuscode,
1726
0
                           int unchanged_kind, int truncated) {
1727
0
  wbuf http;
1728
0
  char resp_id[64];
1729
0
  char pdig[33];
1730
0
  int have_pdig;
1731
0
  int is_revisit = 0;
1732
0
  const char *profile = NULL;
1733
0
  const char *refers_uri = NULL;
1734
0
  const char *refers_date = NULL;
1735
0
  char refers_buf[HTS_URLMAXSIZE * 2 + 64];
1736
0
  int has_payload;
1737
0
  int emit_body;
1738
0
  int rc = -1;
1739
0
  char statusbuf[16];
1740
0
  char mimebuf[256];
1741
1742
0
  if (resp_hdr == NULL)
1743
0
    return -1;
1744
1745
  /* CDXJ status/mime (from the caller's status and the response Content-Type).
1746
   */
1747
0
  snprintf(statusbuf, sizeof(statusbuf), "%d", statuscode);
1748
0
  http_header_value(resp_hdr, "Content-Type", mimebuf, sizeof(mimebuf));
1749
  /* a 304 declares no type, so index the one only the caller knows */
1750
0
  if (mimebuf[0] == '\0' && content_type != NULL)
1751
0
    strlncatbuff(mimebuf, content_type, sizeof(mimebuf), sizeof(mimebuf) - 1);
1752
1753
  /* A payload exists (for digesting) unless this is a bodyless 304. */
1754
0
  has_payload = (body_len > 0 && (body != NULL || body_path != NULL) &&
1755
0
                 unchanged_kind != WARC_UNCHANGED_SERVER_304);
1756
1757
  /* Payload digest drives identical-payload-digest dedup (OpenSSL only). */
1758
0
  have_pdig =
1759
0
      has_payload ? payload_digest_b32(body, body_len, body_path, pdig) : 0;
1760
1761
0
  if (unchanged_kind == WARC_UNCHANGED_SERVER_304) {
1762
0
    is_revisit = 1;
1763
0
    profile = "http://netpreserve.org/warc/1.1/revisit/server-not-modified";
1764
    /* Replay resolves a revisit by this field alone, and a 304 stands in for
1765
       the same URL. No WARC-Refers-To-Date to go with it: the cache keeps the
1766
       document's Last-Modified, never the previous capture time. */
1767
0
    refers_uri = target_uri;
1768
    /* Served from cache: the payload sits in the previous archive, not here. */
1769
0
    w->unbacked_revisits++;
1770
0
  } else if (unchanged_kind == WARC_UNCHANGED_ENGINE_FORCED) {
1771
    /* has_payload requires body_len>0, so a genuinely empty body looks
1772
       digest-less too; it still has a well-defined digest (sha1 of nothing),
1773
       so compute it here rather than treat it as a missing-crypto case. */
1774
0
    if (!have_pdig && body_len == 0 && (body != NULL || body_path != NULL))
1775
0
      have_pdig = payload_digest_b32(body, body_len, body_path, pdig);
1776
    /* Served from cache with no exchange either way: still unbacked. */
1777
0
    w->unbacked_revisits++;
1778
    /* No digest (no OpenSSL) means nothing to point a revisit at, and there
1779
       was no real exchange to record as a response; write nothing (#839). */
1780
0
    if (!have_pdig)
1781
0
      return 0;
1782
0
    is_revisit = 1;
1783
0
    profile =
1784
0
        "http://netpreserve.org/warc/1.1/revisit/identical-payload-digest";
1785
0
    refers_uri = target_uri;
1786
0
  } else if (have_pdig && w->seen != NULL) {
1787
0
    void *prev = NULL;
1788
0
    if (coucal_read_pvoid(w->seen, pdig, &prev) && prev != NULL) {
1789
0
      char *slot = (char *) prev;
1790
0
      char *sep = strchr(slot, '\001');
1791
0
      is_revisit = 1;
1792
0
      profile =
1793
0
          "http://netpreserve.org/warc/1.1/revisit/identical-payload-digest";
1794
0
      if (sep != NULL) {
1795
0
        size_t n = (size_t) (sep - slot);
1796
0
        if (n < sizeof(refers_buf)) {
1797
0
          memcpy(refers_buf, slot, n);
1798
0
          refers_buf[n] = '\0';
1799
0
          refers_uri = refers_buf;
1800
0
          refers_date = sep + 1;
1801
0
        }
1802
0
      }
1803
0
    }
1804
0
  }
1805
1806
  /* Both revisit kinds (server-304 and identical-payload-digest) are bodyless;
1807
     only a full response carries the payload (F1). */
1808
0
  emit_body = has_payload && !is_revisit;
1809
1810
  /* Full response rewrites Content-Length and keeps Content-Encoding verbatim
1811
     (see normalize_http_headers); a revisit keeps the original headers. */
1812
0
  memset(&http, 0, sizeof(http));
1813
0
  if (normalize_http_headers(resp_hdr, emit_body ? (long long) body_len : -1,
1814
0
                             &http) != 0) {
1815
0
    wbuf_free(&http);
1816
0
    return -1;
1817
0
  }
1818
1819
  /* Response first: its id links the request via WARC-Concurrent-To. A revisit
1820
     is a deliberate dedup, not a truncation, so tag WARC-Truncated only on a
1821
     full (body-carrying) response. */
1822
0
  resp_id[0] = '\0';
1823
0
  if (warc_emit(w, is_revisit ? "revisit" : "response",
1824
0
                "application/http;msgtype=response", target_uri, ip, NULL,
1825
0
                refers_uri, refers_date, profile, have_pdig ? pdig : NULL,
1826
0
                emit_body ? warc_truncated_reason(truncated) : NULL, statusbuf,
1827
0
                mimebuf, 1, http.data, http.len, emit_body, body, body_len,
1828
0
                body_path, resp_id) != 0) {
1829
0
    wbuf_free(&http);
1830
0
    return -1;
1831
0
  }
1832
0
  wbuf_free(&http);
1833
1834
0
  if (req_hdr != NULL && req_hdr[0] != '\0') {
1835
0
    size_t rlen = strlen(req_hdr);
1836
0
    if (warc_emit(w, "request", "application/http;msgtype=request", target_uri,
1837
0
                  NULL, resp_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0,
1838
0
                  NULL, 0, 1, req_hdr, rlen, NULL, NULL) != 0)
1839
0
      return -1;
1840
0
  }
1841
1842
  /* Record this payload for later identical-payload-digest revisits. */
1843
0
  if (!is_revisit && have_pdig && w->seen != NULL && target_uri != NULL) {
1844
0
    char date[32];
1845
0
    char *slot;
1846
0
    size_t need;
1847
0
    hts_now_iso8601(date);
1848
0
    need = strlen(target_uri) + 1 + strlen(date) + 1;
1849
0
    slot = malloct(need);
1850
0
    if (slot != NULL) {
1851
0
      snprintf(slot, need, "%s\001%s", target_uri, date);
1852
0
      if (coucal_write_pvoid(w->seen, pdig, slot) == 0) {
1853
        /* replaced an existing entry: coucal freed the old value */
1854
0
      }
1855
0
    }
1856
0
  }
1857
1858
0
  (void) statuscode;
1859
0
  rc = 0;
1860
0
  return rc;
1861
0
}
1862
1863
/* ---- one non-HTTP capture ---- */
1864
1865
int warc_write_resource(warc_writer *w, const char *target_uri, const char *ip,
1866
                        const char *content_type, const char *body,
1867
0
                        size_t body_len, const char *body_path, int truncated) {
1868
0
  char pdig[33];
1869
0
  int has_body = (body_len > 0 && (body != NULL || body_path != NULL));
1870
0
  int have_pdig =
1871
0
      has_body ? payload_digest_b32(body, body_len, body_path, pdig) : 0;
1872
  /* resource: the block is the raw payload, its own MIME is the record's
1873
     Content-Type, and there is no HTTP request/response envelope. */
1874
0
  return warc_emit(w, "resource",
1875
0
                   (content_type != NULL && content_type[0] != '\0')
1876
0
                       ? content_type
1877
0
                       : "application/octet-stream",
1878
0
                   target_uri, ip, NULL, NULL, NULL, NULL,
1879
0
                   have_pdig ? pdig : NULL, warc_truncated_reason(truncated),
1880
0
                   NULL, content_type, 0, NULL, 0, has_body, body, body_len,
1881
0
                   body_path, NULL);
1882
0
}
1883
1884
/* ---- engine emit hook ---- */
1885
1886
0
void warc_write_backtransaction(httrackp *opt, lien_back *back) {
1887
0
  warc_writer *w;
1888
0
  char uri[HTS_URLMAXSIZE * 4 + 16];
1889
0
  char ip[128];
1890
0
  const char *body;
1891
0
  size_t body_len;
1892
0
  const char *body_path;
1893
0
  const char *resp_hdr;
1894
0
  char synth[512];
1895
0
  int unchanged_kind;
1896
0
  int is_ftp;
1897
1898
0
  if (opt->state.warc == WARC_DISABLED)
1899
0
    return;
1900
0
  if (opt->state.warc == NULL) {
1901
0
    w = warc_open(opt, StringBuff(opt->warc_file));
1902
0
    if (w == NULL) {
1903
0
      opt->state.warc = WARC_DISABLED;
1904
0
      hts_log_print(opt, LOG_ERROR, "could not create WARC archive %s",
1905
0
                    StringBuff(opt->warc_file));
1906
0
      return;
1907
0
    }
1908
0
    opt->state.warc = w;
1909
0
  }
1910
0
  w = (warc_writer *) opt->state.warc;
1911
1912
0
  if (back->r.statuscode <= 0)
1913
0
    return;
1914
1915
0
  is_ftp = strfield(back->url_adr, "ftp://") != 0;
1916
0
  snprintf(uri, sizeof(uri), "%s%s%s",
1917
0
           link_has_authority(back->url_adr) ? "" : "http://", back->url_adr,
1918
0
           back->url_fil);
1919
1920
0
  ip[0] = '\0';
1921
0
  SOCaddr_inetntoa(ip, sizeof(ip), back->r.address);
1922
1923
0
  if (!back->r.is_write) {
1924
0
    body = back->r.adr;
1925
0
    body_len = (back->r.size > 0) ? (size_t) back->r.size : 0;
1926
0
    body_path = NULL;
1927
0
  } else {
1928
0
    LLint fs;
1929
0
    body = NULL;
1930
0
    body_path = back->url_sav;
1931
0
    fs = fsize_utf8(body_path);
1932
    /* F4: an on-disk body past size_t (LLP32/ILP32, >4GB) would wrap the length
1933
       used as Content-Length; drop the body rather than desync the record. */
1934
0
    if (fs > 0 && (uint64_t) fs <= (uint64_t) (size_t) -1)
1935
0
      body_len = (size_t) fs;
1936
0
    else
1937
0
      body_len = 0;
1938
0
  }
1939
1940
  /* FTP has no HTTP envelope: one resource record carrying the payload. */
1941
0
  if (is_ftp) {
1942
0
    warc_write_resource(w, uri, ip, back->r.contenttype, body, body_len,
1943
0
                        body_path, back->r.warc_truncated);
1944
0
    return;
1945
0
  }
1946
1947
  /* Prefer the spooled coded body so the record stores it verbatim; the digest
1948
     is then over the coded payload. */
1949
0
  if (back->r.warc_rawpath != NULL && back->r.warc_rawsize > 0 &&
1950
0
      (uint64_t) back->r.warc_rawsize <= (uint64_t) (size_t) -1) {
1951
0
    body = NULL;
1952
0
    body_path = back->r.warc_rawpath;
1953
0
    body_len = (size_t) back->r.warc_rawsize;
1954
0
  }
1955
1956
0
  if (!back->r.notmodified || !opt->is_update)
1957
0
    unchanged_kind = WARC_UNCHANGED_NONE;
1958
0
  else if (back->r.warc_forced_notmodified)
1959
0
    unchanged_kind = WARC_UNCHANGED_ENGINE_FORCED;
1960
0
  else
1961
0
    unchanged_kind = WARC_UNCHANGED_SERVER_304;
1962
1963
  /* Prefer the stashed raw headers; synthesize a minimal status line for the
1964
     header-less (HTTP/0.9-style) responses that never carried a header block.
1965
   */
1966
0
  resp_hdr = back->r.warc_resphdr;
1967
0
  if (resp_hdr == NULL) {
1968
0
    snprintf(synth, sizeof(synth), "HTTP/1.1 %d %s\r\nContent-Type: %s\r\n\r\n",
1969
0
             back->r.statuscode, back->r.msg[0] ? back->r.msg : "OK",
1970
0
             back->r.contenttype[0] ? back->r.contenttype
1971
0
                                    : "application/octet-stream");
1972
0
    resp_hdr = synth;
1973
0
  }
1974
1975
0
  warc_write_transaction(w, uri, ip, back->r.warc_reqhdr, resp_hdr, body,
1976
0
                         body_len, body_path, back->r.contenttype,
1977
0
                         back->r.statuscode, unchanged_kind,
1978
0
                         back->r.warc_truncated);
1979
0
}