g_ptr_array_free(arch->files, TRUE);
}
-static inline guint16
-rspamd_zip_time_dos(time_t t)
-{
- struct tm lt;
-
- if (t == 0) {
- t = time(NULL);
- }
-
- (void) localtime_r(&t, <);
-
- guint16 dos_time = ((guint16) (lt.tm_hour & 0x1f) << 11) |
- ((guint16) (lt.tm_min & 0x3f) << 5) |
- ((guint16) ((lt.tm_sec / 2) & 0x1f));
-
- return dos_time;
-}
-
-static inline guint16
-rspamd_zip_date_dos(time_t t)
-{
- struct tm lt;
-
- if (t == 0) {
- t = time(NULL);
- }
-
- (void) localtime_r(&t, <);
-
- int year = lt.tm_year + 1900;
- if (year < 1980) {
- year = 1980; /* DOS date epoch */
- }
-
- guint16 dos_date = ((guint16) ((year - 1980) & 0x7f) << 9) |
- ((guint16) ((lt.tm_mon + 1) & 0x0f) << 5) |
- ((guint16) (lt.tm_mday & 0x1f));
-
- return dos_date;
-}
-
-static inline void
-rspamd_ba_append_u16le(GByteArray *ba, guint16 v)
-{
- union {
- guint16 u16;
- unsigned char b[2];
- } u;
-
- u.u16 = GUINT16_TO_LE(v);
- g_byte_array_append(ba, u.b, sizeof(u.b));
-}
-
-static inline void
-rspamd_ba_append_u32le(GByteArray *ba, guint32 v)
-{
- union {
- guint32 u32;
- unsigned char b[4];
- } u;
-
- u.u32 = GUINT32_TO_LE(v);
- g_byte_array_append(ba, u.b, sizeof(u.b));
-}
-
-static gboolean
-rspamd_zip_deflate_alloc(const unsigned char *in,
- gsize inlen,
- unsigned char **outbuf,
- gsize *outlen)
-{
- int rc;
- z_stream strm;
-
- memset(&strm, 0, sizeof(strm));
- /* raw DEFLATE stream for ZIP */
- rc = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
- -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY);
-
- if (rc != Z_OK) {
- return FALSE;
- }
-
- /* Compute upper bound and allocate */
- uLong bound = deflateBound(&strm, (uLong) inlen);
- unsigned char *obuf = g_malloc(bound);
-
- strm.next_in = (unsigned char *) in;
- strm.avail_in = inlen;
- strm.next_out = obuf;
- strm.avail_out = bound;
-
- rc = deflate(&strm, Z_FINISH);
-
- if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
- deflateEnd(&strm);
- g_free(obuf);
- return FALSE;
- }
-
- *outlen = bound - strm.avail_out;
- *outbuf = obuf;
- deflateEnd(&strm);
-
- return TRUE;
-}
-
-static gboolean
-rspamd_zip_validate_name(const char *name)
-{
- if (name == NULL || *name == '\0') {
- return FALSE;
- }
- /* Disallow absolute paths and parent traversals */
- if (name[0] == '/' || name[0] == '\\') {
- return FALSE;
- }
- if (strstr(name, "..") != NULL) {
- return FALSE;
- }
- if (strchr(name, ':') != NULL) {
- return FALSE;
- }
-
- return TRUE;
-}
-
-static void
-rspamd_zip_write_local_header(GByteArray *zip,
- const char *name,
- guint16 ver_needed,
- guint16 gp_flags,
- guint16 method,
- time_t mtime,
- guint32 crc,
- guint32 csize,
- guint32 usize,
- guint16 extra_len)
-{
- /* Local file header */
- /* signature */
- rspamd_ba_append_u32le(zip, 0x04034b50);
- /* version needed to extract */
- rspamd_ba_append_u16le(zip, ver_needed);
- /* general purpose bit flag */
- rspamd_ba_append_u16le(zip, gp_flags);
- /* compression method */
- rspamd_ba_append_u16le(zip, method);
- /* last mod file time/date */
- rspamd_ba_append_u16le(zip, rspamd_zip_time_dos(mtime));
- rspamd_ba_append_u16le(zip, rspamd_zip_date_dos(mtime));
- /* CRC-32 */
- rspamd_ba_append_u32le(zip, crc);
- /* compressed size */
- rspamd_ba_append_u32le(zip, csize);
- /* uncompressed size */
- rspamd_ba_append_u32le(zip, usize);
- /* file name length */
- rspamd_ba_append_u16le(zip, (guint16) strlen(name));
- /* extra field length */
- rspamd_ba_append_u16le(zip, extra_len);
- /* file name */
- g_byte_array_append(zip, (const guint8 *) name, strlen(name));
-}
-
-static void
-rspamd_zip_write_central_header(GByteArray *cd,
- const char *name,
- guint16 ver_needed,
- guint16 gp_flags,
- guint16 method,
- time_t mtime,
- guint32 crc,
- guint32 csize,
- guint32 usize,
- guint32 lfh_offset,
- guint32 mode,
- guint16 extra_len)
-{
- /* Central directory file header */
- rspamd_ba_append_u32le(cd, 0x02014b50);
- /* version made by: 3 (UNIX) << 8 | 20 */
- rspamd_ba_append_u16le(cd, (guint16) ((3 << 8) | 20));
- /* version needed to extract */
- rspamd_ba_append_u16le(cd, ver_needed);
- /* general purpose bit flag */
- rspamd_ba_append_u16le(cd, gp_flags);
- /* compression method */
- rspamd_ba_append_u16le(cd, method);
- /* time/date */
- rspamd_ba_append_u16le(cd, rspamd_zip_time_dos(mtime));
- rspamd_ba_append_u16le(cd, rspamd_zip_date_dos(mtime));
- /* CRC and sizes */
- rspamd_ba_append_u32le(cd, crc);
- rspamd_ba_append_u32le(cd, csize);
- rspamd_ba_append_u32le(cd, usize);
- /* name len, extra len, comment len */
- rspamd_ba_append_u16le(cd, (guint16) strlen(name));
- rspamd_ba_append_u16le(cd, extra_len);
- rspamd_ba_append_u16le(cd, 0);
- /* disk number start, internal attrs */
- rspamd_ba_append_u16le(cd, 0);
- rspamd_ba_append_u16le(cd, 0);
- /* external attrs: UNIX perms in upper 16 bits */
- guint32 xattr = (mode ? mode : 0644);
- xattr = (xattr & 0xFFFF) << 16;
- rspamd_ba_append_u32le(cd, xattr);
- /* relative offset of local header */
- rspamd_ba_append_u32le(cd, lfh_offset);
- /* file name */
- g_byte_array_append(cd, (const guint8 *) name, strlen(name));
-}
-
-/* --- ZipCrypto (PKWARE traditional) helpers --- */
-static const guint32 rspamd_zip_crc32_tab[256] = {
- 0x00000000U, 0x77073096U, 0xEE0E612CU, 0x990951BAU, 0x076DC419U, 0x706AF48FU, 0xE963A535U, 0x9E6495A3U,
- 0x0EDB8832U, 0x79DCB8A4U, 0xE0D5E91EU, 0x97D2D988U, 0x09B64C2BU, 0x7EB17CBDU, 0xE7B82D07U, 0x90BF1D91U,
- 0x1DB71064U, 0x6AB020F2U, 0xF3B97148U, 0x84BE41DEU, 0x1ADAD47DU, 0x6DDDE4EBU, 0xF4D4B551U, 0x83D385C7U,
- 0x136C9856U, 0x646BA8C0U, 0xFD62F97AU, 0x8A65C9ECU, 0x14015C4FU, 0x63066CD9U, 0xFA0F3D63U, 0x8D080DF5U,
- 0x3B6E20C8U, 0x4C69105EU, 0xD56041E4U, 0xA2677172U, 0x3C03E4D1U, 0x4B04D447U, 0xD20D85FDU, 0xA50AB56BU,
- 0x35B5A8FAU, 0x42B2986CU, 0xDBBBC9D6U, 0xACBCF940U, 0x32D86CE3U, 0x45DF5C75U, 0xDCD60DCFU, 0xABD13D59U,
- 0x26D930ACU, 0x51DE003AU, 0xC8D75180U, 0xBFD06116U, 0x21B4F4B5U, 0x56B3C423U, 0xCFBA9599U, 0xB8BDA50FU,
- 0x2802B89EU, 0x5F058808U, 0xC60CD9B2U, 0xB10BE924U, 0x2F6F7C87U, 0x58684C11U, 0xC1611DABU, 0xB6662D3DU,
- 0x76DC4190U, 0x01DB7106U, 0x98D220BCU, 0xEFD5102AU, 0x71B18589U, 0x06B6B51FU, 0x9FBFE4A5U, 0xE8B8D433U,
- 0x7807C9A2U, 0x0F00F934U, 0x9609A88EU, 0xE10E9818U, 0x7F6A0DBBU, 0x086D3D2DU, 0x91646C97U, 0xE6635C01U,
- 0x6B6B51F4U, 0x1C6C6162U, 0x856530D8U, 0xF262004EU, 0x6C0695EDU, 0x1B01A57BU, 0x8208F4C1U, 0xF50FC457U,
- 0x65B0D9C6U, 0x12B7E950U, 0x8BBEB8EAU, 0xFCB9887CU, 0x62DD1DDFU, 0x15DA2D49U, 0x8CD37CF3U, 0xFBD44C65U,
- 0x4DB26158U, 0x3AB551CEU, 0xA3BC0074U, 0xD4BB30E2U, 0x4ADFA541U, 0x3DD895D7U, 0xA4D1C46DU, 0xD3D6F4FBU,
- 0x4369E96AU, 0x346ED9FCU, 0xAD678846U, 0xDA60B8D0U, 0x44042D73U, 0x33031DE5U, 0xAA0A4C5FU, 0xDD0D7CC9U,
- 0x5005713CU, 0x270241AAU, 0xBE0B1010U, 0xC90C2086U, 0x5768B525U, 0x206F85B3U, 0xB966D409U, 0xCE61E49FU,
- 0x5EDEF90EU, 0x29D9C998U, 0xB0D09822U, 0xC7D7A8B4U, 0x59B33D17U, 0x2EB40D81U, 0xB7BD5C3BU, 0xC0BA6CADU,
- 0xEDB88320U, 0x9ABFB3B6U, 0x03B6E20CU, 0x74B1D29AU, 0xEAD54739U, 0x9DD277AFU, 0x04DB2615U, 0x73DC1683U,
- 0xE3630B12U, 0x94643B84U, 0x0D6D6A3EU, 0x7A6A5AA8U, 0xE40ECF0BU, 0x9309FF9DU, 0x0A00AE27U, 0x7D079EB1U,
- 0xF00F9344U, 0x8708A3D2U, 0x1E01F268U, 0x6906C2FEU, 0xF762575DU, 0x806567CBU, 0x196C3671U, 0x6E6B06E7U,
- 0xFED41B76U, 0x89D32BE0U, 0x10DA7A5AU, 0x67DD4ACCU, 0xF9B9DF6FU, 0x8EBEEFF9U, 0x17B7BE43U, 0x60B08ED5U,
- 0xD6D6A3E8U, 0xA1D1937EU, 0x38D8C2C4U, 0x4FDFF252U, 0xD1BB67F1U, 0xA6BC5767U, 0x3FB506DDU, 0x48B2364BU,
- 0xD80D2BDAU, 0xAF0A1B4CU, 0x36034AF6U, 0x41047A60U, 0xDF60EFC3U, 0xA867DF55U, 0x316E8EEFU, 0x4669BE79U,
- 0xCB61B38CU, 0xBC66831AU, 0x256FD2A0U, 0x5268E236U, 0xCC0C7795U, 0xBB0B4703U, 0x220216B9U, 0x5505262FU,
- 0xC5BA3BBEU, 0xB2BD0B28U, 0x2BB45A92U, 0x5CB36A04U, 0xC2D7FFA7U, 0xB5D0CF31U, 0x2CD99E8BU, 0x5BDEAE1DU,
- 0x9B64C2B0U, 0xEC63F226U, 0x756AA39CU, 0x026D930AU, 0x9C0906A9U, 0xEB0E363FU, 0x72076785U, 0x05005713U,
- 0x95BF4A82U, 0xE2B87A14U, 0x7BB12BAEU, 0x0CB61B38U, 0x92D28E9BU, 0xE5D5BE0DU, 0x7CDCEFB7U, 0x0BDBDF21U,
- 0x86D3D2D4U, 0xF1D4E242U, 0x68DDB3F8U, 0x1FDA836EU, 0x81BE16CDU, 0xF6B9265BU, 0x6FB077E1U, 0x18B74777U,
- 0x88085AE6U, 0xFF0F6A70U, 0x66063BCAU, 0x11010B5CU, 0x8F659EFFU, 0xF862AE69U, 0x616BFFD3U, 0x166CCF45U,
- 0xA00AE278U, 0xD70DD2EEU, 0x4E048354U, 0x3903B3C2U, 0xA7672661U, 0xD06016F7U, 0x4969474DU, 0x3E6E77DBU,
- 0xAED16A4AU, 0xD9D65ADCU, 0x40DF0B66U, 0x37D83BF0U, 0xA9BCAE53U, 0xDEBB9EC5U, 0x47B2CF7FU, 0x30B5FFE9U,
- 0xBDBDF21CU, 0xCABAC28AU, 0x53B39330U, 0x24B4A3A6U, 0xBAD03605U, 0xCDD70693U, 0x54DE5729U, 0x23D967BFU,
- 0xB3667A2EU, 0xC4614AB8U, 0x5D681B02U, 0x2A6F2B94U, 0xB40BBE37U, 0xC30C8EA1U, 0x5A05DF1BU, 0x2D02EF8DU};
-
-static inline guint32
-rspamd_zip_crc32_update(guint32 crc, guint8 c)
-{
- return rspamd_zip_crc32_tab[(crc ^ c) & 0xff] ^ (crc >> 8);
-}
-static inline void
-rspamd_zipcrypto_init_keys(guint32 keys[3])
-{
- keys[0] = 0x12345678UL;
- keys[1] = 0x23456789UL;
- keys[2] = 0x34567890UL;
-}
-
-static inline void
-rspamd_zipcrypto_update_keys(guint32 keys[3], guint8 c)
-{
- keys[0] = rspamd_zip_crc32_update(keys[0], c);
- keys[1] = (keys[1] + (keys[0] & 0xff));
- keys[1] = keys[1] * 134775813UL + 1;
- guint8 t = (keys[1] >> 24) & 0xff;
- keys[2] = rspamd_zip_crc32_update(keys[2], t);
-}
-
-static inline guint8
-rspamd_zipcrypto_crypt_byte(const guint32 keys[3])
-{
- guint16 t = (guint16) ((keys[2] & 0xffff) | 2);
- return (guint8) (((t * (t ^ 1)) >> 8) & 0xff);
-}
-
-static inline void
-rspamd_zipcrypto_init_with_password(guint32 keys[3], const char *password)
-{
- rspamd_zipcrypto_init_keys(keys);
- if (password != NULL) {
- const unsigned char *p = (const unsigned char *) password;
- while (*p) {
- rspamd_zipcrypto_update_keys(keys, *p++);
- }
- }
-}
-
-GByteArray *
-rspamd_archives_zip_write(const struct rspamd_zip_file_spec *files,
- gsize nfiles,
- const char *password,
- GError **err)
-{
- GByteArray *zip = NULL, *cd = NULL;
- GQuark q = rspamd_archives_err_quark();
-
- if (files == NULL || nfiles == 0) {
- g_set_error(err, q, EINVAL, "no files to archive");
- return NULL;
- }
-
- zip = g_byte_array_new();
- cd = g_byte_array_new();
-
- for (gsize i = 0; i < nfiles; i++) {
- const struct rspamd_zip_file_spec *f = &files[i];
- if (!rspamd_zip_validate_name(f->name)) {
- g_set_error(err, q, EINVAL, "invalid zip entry name: %s", f->name ? f->name : "(null)");
- g_byte_array_free(cd, TRUE);
- g_byte_array_free(zip, TRUE);
- return NULL;
- }
-
- guint32 crc = crc32(0L, Z_NULL, 0);
- crc = crc32(crc, f->data, f->len);
- guint16 method = 8; /* deflate */
- guint16 gp_flags = (1u << 11); /* UTF-8 */
- guint16 ver_needed = 20; /* default */
- const gboolean use_zipcrypto = (password != NULL && *password != '\0');
-
- /* actual method will be decided after deflate; default is deflate */
-
- guint16 extra_len = 0;
- guint32 csize_for_header = 0;
- gboolean use_descriptor = FALSE;
- if (use_zipcrypto) {
- /* Traditional PKWARE ZipCrypto */
- gp_flags |= 1u; /* encrypted */
- gp_flags |= (1u << 3); /* data descriptor present */
- use_descriptor = TRUE;
- /* method remains 8 or 0 depending on compression effectiveness */
- /* no extra field */
- }
-
- guint32 lfh_off = zip->len;
- rspamd_zip_write_local_header(zip, f->name, ver_needed, gp_flags, method, f->mtime,
- use_descriptor ? 0 : crc,
- use_descriptor ? 0 : csize_for_header,
- use_descriptor ? 0 : (guint32) f->len,
- extra_len);
- msg_debug_archive_taskless("lfh: off=%d ver_needed=%d gp_flags=%d method=%d name_len=%d extra_len=%d",
- (int) lfh_off, (int) ver_needed, (int) gp_flags, (int) method,
- (int) strlen(f->name), (int) extra_len);
- if (use_zipcrypto) {
- /* Prepare ZipCrypto keys */
- guint32 keys[3];
- rspamd_zipcrypto_init_with_password(keys, password);
-
- /* Build 12-byte encryption header */
- guint8 hdr[12];
- ottery_rand_bytes(hdr, sizeof(hdr));
- /* set verification bytes */
- if (use_descriptor) {
- /* when bit 3 is set, use MS-DOS time field */
- guint16 dos_t = rspamd_zip_time_dos(f->mtime);
- hdr[10] = (guint8) (dos_t & 0xff);
- hdr[11] = (guint8) ((dos_t >> 8) & 0xff);
- }
- else {
- /* high 2 bytes of CRC32 of plaintext */
- hdr[10] = (guint8) ((crc >> 16) & 0xff);
- hdr[11] = (guint8) ((crc >> 24) & 0xff);
- }
- /* Encrypt header in place */
- for (guint i = 0; i < sizeof(hdr); i++) {
- guint8 k = rspamd_zipcrypto_crypt_byte(keys);
- guint8 c = hdr[i] ^ k;
- hdr[i] = c;
- /* update keys with header plaintext byte */
- rspamd_zipcrypto_update_keys(keys, (guint8) (c ^ k));
- }
- g_byte_array_append(zip, hdr, sizeof(hdr));
-
- /* Now compress directly into zip buffer (in place) or store */
- gsize produced = 0;
- gboolean used_deflate = TRUE;
- guint32 data_off = zip->len; /* start of (plaintext) data before encryption */
-
- /* Try to reserve space by deflateBound and compress into zip */
- z_stream zst;
- memset(&zst, 0, sizeof(zst));
- if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) == Z_OK) {
- uLong bound = deflateBound(&zst, (uLong) f->len);
- deflateEnd(&zst);
-
- /* Reserve space */
- g_byte_array_set_size(zip, data_off + bound);
-
- memset(&zst, 0, sizeof(zst));
- if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
- /* fallback to store */
- used_deflate = FALSE;
- }
- else {
- zst.next_in = (unsigned char *) f->data;
- zst.avail_in = f->len;
- zst.next_out = zip->data + data_off;
- zst.avail_out = bound;
- int rc = deflate(&zst, Z_FINISH);
- if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
- used_deflate = FALSE;
- deflateEnd(&zst);
- }
- else {
- produced = bound - zst.avail_out;
- deflateEnd(&zst);
- if (produced >= f->len) {
- used_deflate = FALSE;
- }
- }
- }
- }
- else {
- used_deflate = FALSE;
- }
-
- if (!used_deflate) {
- /* Store: reset to data_off and copy original data */
- g_byte_array_set_size(zip, data_off);
- g_byte_array_set_size(zip, data_off + f->len);
- memcpy(zip->data + data_off, f->data, f->len);
- produced = f->len;
- /* patch method in local header (offset +8) */
- guint16 *pm = (guint16 *) (zip->data + lfh_off + 8);
- method = 0;
- *pm = GUINT16_TO_LE(method);
- }
-
- /* Encrypt in place over zip->data[data_off .. data_off+produced) */
- for (gsize i = 0; i < produced; i++) {
- guint8 k = rspamd_zipcrypto_crypt_byte(keys);
- guint8 pt = zip->data[data_off + i];
- zip->data[data_off + i] = pt ^ k;
- rspamd_zipcrypto_update_keys(keys, pt);
- }
- /* Shrink to actual size (if deflated) */
- g_byte_array_set_size(zip, data_off + produced);
-
- /* compressed size includes 12-byte header + encrypted data */
- csize_for_header = (guint32) (12 + produced);
- if (!use_descriptor) {
- /* patch CRC (offset +14) and compressed size (offset +18) */
- guint32 *p32 = (guint32 *) (zip->data + lfh_off + 14);
- *p32 = GUINT32_TO_LE(crc);
- p32 = (guint32 *) (zip->data + lfh_off + 18);
- *p32 = GUINT32_TO_LE(csize_for_header);
- /* uncompressed size already set in LFH */
- }
- else {
- /* append data descriptor with signature */
- rspamd_ba_append_u32le(zip, 0x08074b50);
- rspamd_ba_append_u32le(zip, crc);
- rspamd_ba_append_u32le(zip, csize_for_header);
- rspamd_ba_append_u32le(zip, (guint32) f->len);
- }
-
- msg_debug_archive_taskless("zip-zipcrypto: added entry '%s' (usize=%L, csize=%L, method=%s)",
- f->name, (int64_t) f->len, (int64_t) csize_for_header,
- used_deflate ? "deflate+zipcrypto" : "store+zipcrypto");
- }
- else {
- /* Not encrypted: deflate directly into zip, fallback to store */
- z_stream zst;
- memset(&zst, 0, sizeof(zst));
- if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
- g_set_error(err, q, EIO, "deflateInit2 failed");
- return NULL;
- }
- uLong bound = deflateBound(&zst, (uLong) f->len);
- deflateEnd(&zst);
- gsize off = zip->len;
- g_byte_array_set_size(zip, zip->len + bound);
- unsigned char *outp = zip->data + off;
- memset(&zst, 0, sizeof(zst));
- if (deflateInit2(&zst, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL - 1, Z_DEFAULT_STRATEGY) != Z_OK) {
- g_set_error(err, q, EIO, "deflateInit2 failed");
- return NULL;
- }
- zst.next_in = (unsigned char *) f->data;
- zst.avail_in = f->len;
- zst.next_out = outp;
- zst.avail_out = bound;
- int rc = deflate(&zst, Z_FINISH);
- if (rc != Z_STREAM_END && rc != Z_OK && rc != Z_BUF_ERROR) {
- deflateEnd(&zst);
- g_set_error(err, q, EIO, "deflate failed");
- return NULL;
- }
- gsize produced = bound - zst.avail_out;
- deflateEnd(&zst);
- if (produced >= f->len) {
- /* store */
- g_byte_array_set_size(zip, off);
- g_byte_array_set_size(zip, zip->len + f->len);
- memcpy(zip->data + off, f->data, f->len);
- produced = f->len;
- method = 0;
- /* patch method in local header (offset +8) */
- guint16 *pm = (guint16 *) (zip->data + lfh_off + 8);
- *pm = GUINT16_TO_LE(method);
- msg_debug_archive_taskless("zip: fallback to store (no encryption) - deflated=%L, original=%L",
- (int64_t) (bound - zst.avail_out), (int64_t) f->len);
- }
- else {
- g_byte_array_set_size(zip, off + produced);
- }
- csize_for_header = (guint32) produced;
- /* patch CRC (offset +14) and compressed size (offset +18) */
- guint32 *p32 = (guint32 *) (zip->data + lfh_off + 14);
- *p32 = GUINT32_TO_LE(crc);
- p32 = (guint32 *) (zip->data + lfh_off + 18);
- *p32 = GUINT32_TO_LE(csize_for_header);
- }
-
- guint32 cd_off = cd->len;
- rspamd_zip_write_central_header(cd, f->name, ver_needed, gp_flags, method, f->mtime, crc,
- csize_for_header,
- (guint32) f->len,
- lfh_off, f->mode, extra_len);
- msg_debug_archive_taskless("cd_entry: off=%d lfh_off=%d name=%s csize=%d usize=%d",
- (int) cd_off, (int) lfh_off, f->name, (int) csize_for_header, (int) f->len);
- msg_debug_archive_taskless("cd: ver_needed=%d gp_flags=%d method=%d csize=%L usize=%L",
- (int) ver_needed, (int) gp_flags, (int) method,
- (int64_t) csize_for_header, (int64_t) f->len);
-
- guint64 logged_csize = (guint64) csize_for_header;
- const char *method_str;
- method_str = (use_zipcrypto ? (method == 0 ? "store+zipcrypto" : "deflate+zipcrypto")
- : (method == 0 ? "store" : "deflate"));
- msg_debug_archive_taskless("zip: added entry '%s' (usize=%L, csize=%L, method=%s)",
- f->name, (int64_t) f->len, (int64_t) logged_csize,
- method_str);
- }
-
- /* Central directory start */
- guint32 cd_start = zip->len;
- g_byte_array_append(zip, cd->data, cd->len);
- guint32 cd_size = cd->len;
- g_byte_array_free(cd, TRUE);
-
- /* EOCD */
- rspamd_ba_append_u32le(zip, 0x06054b50);
- /* disk numbers */
- rspamd_ba_append_u16le(zip, 0);
- rspamd_ba_append_u16le(zip, 0);
- /* total entries on this disk / total entries */
- rspamd_ba_append_u16le(zip, (guint16) nfiles);
- rspamd_ba_append_u16le(zip, (guint16) nfiles);
- /* size of central directory */
- rspamd_ba_append_u32le(zip, cd_size);
- /* offset of central directory */
- rspamd_ba_append_u32le(zip, cd_start);
- /* zip comment length */
- rspamd_ba_append_u16le(zip, 0);
-
- msg_debug_archive_taskless("zip: created archive (%L bytes, cd_start=%d, cd_size=%d)",
- (int64_t) zip->len, (int) cd_start, (int) cd_size);
-
- /* Debug: check archive structure */
- if (zip->len >= 4) {
- guint32 sig = GUINT32_FROM_LE(*(guint32 *) zip->data);
- msg_debug_archive_taskless("zip: first 4 bytes = %xd (should be 4034b50 for PK\\003\\004)", sig);
- }
-
- /* Additional validation */
- if (cd_start + cd_size + 22 != zip->len) {
- msg_debug_archive_taskless("zip: WARNING - archive size mismatch: cd_start(%d) + cd_size(%d) + eocd(22) = %d, but zip->len = %d",
- (int) cd_start, (int) cd_size, (int) (cd_start + cd_size + 22), (int) zip->len);
- }
-
- /* no debug dump */
-
- return zip;
-}
-
-/* removed obsolete whole-archive AES-256-CBC function */
static bool
rspamd_archive_file_try_utf(struct rspamd_task *task,
#include "lua_common.h"
#include "unix-std.h"
-#include "libmime/archives.h"
#include <archive.h>
#include <archive_entry.h>
/***
* @function archive.pack(format, files[, options])
* Packs a list of files into an in-memory archive using libarchive.
- * @param {string} format archive format name (e.g. "zip", "tar", "7zip", ...)
+ *
+ * @param {string} format archive format name (typical: "zip", "tar", "7zip")
* @param {table} files array of tables: { name = string, content = string|rspamd_text, [mode|perms] = int, [mtime] = int }
- * @param {table} options optional table; `filters` can be string or array of strings (e.g. "gzip", "xz", "zstd")
+ * @param {table} options optional table configuring filters and format behavior
+ * - filters: string or array of strings; compression filters to apply (e.g. "gzip", "xz", "zstd", "bzip2")
+ * - password: string passphrase for encrypted formats (ZIP only here)
+ * - format_options: table of format-specific options (alternatively, a nested table named after the format, e.g. options.zip = {...})
+ *
+ * ZIP-specific options (via options.format_options or options.zip):
+ * - encryption: "traditional" (aka "zipcrypt"), "aes128", or "aes256"
+ * - compression: "store" | "deflate"
+ * - compression-level: integer 0..9 (0 implies "store")
+ * - zip64: boolean (true to force Zip64; use with care)
+ * - hdrcharset: character set name for filenames
+ * - experimental, fakecrc32: booleans (testing only; not recommended for production)
+ *
+ * Notes:
+ * - If options.password is set and encryption is omitted, you can specify it explicitly as shown below.
+ * - For a complete list of libarchive ZIP options, consult libarchive documentation.
+ *
* @return {text} archive bytes
- * @example
+ *
+ * @example -- Plain ZIP
* local blob = archive.pack("zip", {
- * { name = "test.txt", content = "Hello" },
- * { name = "dir/readme.md", content = "# Readme" },
- * }, { filters = "zstd" })
+ * { name = "a.txt", content = "Hello" },
+ * })
+ *
+ * @example -- ZIP with ZipCrypto (traditional)
+ * local blob = archive.pack("zip", files, { password = "secret", zip = { encryption = "traditional" } })
+ *
+ * @example -- ZIP with AES-128
+ * local blob = archive.pack("zip", files, { password = "secret", format_options = { encryption = "aes128" } })
+ *
+ * @example -- TAR.GZ
+ * local blob = archive.pack("tar", files, { filters = "gzip" })
*/
LUA_FUNCTION_DEF(archive, pack);
lua_pushfstring(L, "unsupported format: %s", fmt ? fmt : "(nil)");
return FALSE;
}
+
+static gboolean
+lua_archive_set_format_options_table(lua_State *L, struct archive *a, const char *fmt, int idx)
+{
+ gboolean ok = TRUE;
+
+ if (!lua_istable(L, idx)) {
+ return ok;
+ }
+
+ lua_pushnil(L);
+
+ while (lua_next(L, idx)) {
+ const char *key = lua_tostring(L, -2);
+ const char *valstr = NULL;
+ char nb[64];
+
+ if (key) {
+ int t = lua_type(L, -1);
+ if (t == LUA_TSTRING) {
+ valstr = lua_tostring(L, -1);
+ }
+ else if (t == LUA_TNUMBER) {
+ rspamd_snprintf(nb, sizeof(nb), "%l", (long) lua_tointeger(L, -1));
+ valstr = nb;
+ }
+ else if (t == LUA_TBOOLEAN) {
+ valstr = lua_toboolean(L, -1) ? "1" : "0";
+ }
+
+ if (valstr) {
+ int r = archive_write_set_format_option(a, fmt, key, valstr);
+ if (r != ARCHIVE_OK && r != ARCHIVE_WARN) {
+ ok = FALSE;
+ lua_pop(L, 1); /* value */
+ break;
+ }
+ }
+ }
+
+ lua_pop(L, 1); /* value */
+ }
+
+ return ok;
+}
+
+static gboolean
+lua_archive_add_format_options(lua_State *L, struct archive *a, const char *fmt, int opts_idx)
+{
+ gboolean ok = TRUE;
+
+ if (opts_idx <= 0 || !lua_istable(L, opts_idx)) {
+ return ok;
+ }
+
+ /* Optional password */
+ lua_getfield(L, opts_idx, "password");
+ if (lua_isstring(L, -1)) {
+ const char *pw = lua_tostring(L, -1);
+ if (pw && *pw) {
+ int r = archive_write_set_passphrase(a, pw);
+ if (r != ARCHIVE_OK && r != ARCHIVE_WARN) {
+ ok = FALSE;
+ }
+ }
+ }
+ lua_pop(L, 1);
+
+ /* Generic format_options table */
+ lua_getfield(L, opts_idx, "format_options");
+ if (!lua_isnil(L, -1)) {
+ if (!lua_archive_set_format_options_table(L, a, fmt, lua_gettop(L))) {
+ ok = FALSE;
+ }
+ }
+ lua_pop(L, 1);
+
+ /* Also support nested table named after format (e.g. options.zip) */
+ if (fmt) {
+ lua_getfield(L, opts_idx, fmt);
+ if (!lua_isnil(L, -1)) {
+ if (!lua_archive_set_format_options_table(L, a, fmt, lua_gettop(L))) {
+ ok = FALSE;
+ }
+ }
+ lua_pop(L, 1);
+ }
+
+ return ok;
+}
static int
lua_archive_zip(lua_State *L)
{
/***
* @function archive.zip_encrypt(files[, password])
- * Create a ZIP archive in-memory using Rspamd ZIP writer.
- * If password is provided and non-empty, entries are encrypted with traditional ZipCrypto (PKWARE).
- * - Widely compatible (Info-ZIP/unzip/7-Zip/WinZip), but not cryptographically strong
+ * Convenience helper for creating ZIP archives.
+ * - If password is provided and non-empty, uses libarchive with ZIP traditional encryption (ZipCrypto).
+ * - If password is nil/empty, produces a plain (unencrypted) ZIP.
+ * - For AES encryption, prefer archive.pack("zip", files, { password = "...", zip = { encryption = "aes128"|"aes256" } }).
* @param {table} files array: { name = string, content = string|rspamd_text, [mode|perms] = int, [mtime] = int }
* @param {string} password optional password string
* @return {text} archive bytes
lua_archive_zip_encrypt(lua_State *L)
{
LUA_TRACE_POINT;
- luaL_checktype(L, 1, LUA_TTABLE);
+ /* Re-route to libarchive packer with traditional encryption */
+ luaL_checktype(L, 1, LUA_TTABLE); /* files */
const char *password = NULL;
if (lua_gettop(L) >= 2 && !lua_isnil(L, 2)) {
if (lua_type(L, 2) == LUA_TSTRING) {
return luaL_error(L, "invalid password (string expected)");
}
}
- GArray *specs = g_array_sized_new(FALSE, FALSE, sizeof(struct rspamd_zip_file_spec), 8);
- GError *err = NULL;
- /* Iterate files array */
- lua_pushnil(L);
-
- while (lua_next(L, 1)) {
- if (!lua_istable(L, -1)) {
- g_array_free(specs, TRUE);
- return luaL_error(L, "invalid file entry (expected table)");
- }
-
- int item_idx = lua_gettop(L);
- const char *name = NULL;
- const char *sdata = NULL;
- size_t slen = 0;
- time_t mtime = (time_t) 0;
- guint32 mode = 0644;
-
- lua_getfield(L, item_idx, "name");
- name = lua_tostring(L, -1);
- if (name == NULL || *name == '\0') {
- lua_pop(L, 2);
- g_array_free(specs, TRUE);
- return luaL_error(L, "invalid file entry (missing name)");
- }
- char *dupname = g_strdup(name);
- lua_pop(L, 1);
-
- lua_getfield(L, item_idx, "content");
- struct rspamd_lua_text *t = NULL;
- if ((t = lua_check_text_or_string(L, -1)) != NULL) {
- sdata = (const char *) t->start;
- slen = t->len;
- }
- else if (lua_isstring(L, -1)) {
- sdata = lua_tolstring(L, -1, &slen);
- }
- else {
- lua_pop(L, 2);
- g_free(dupname);
- g_array_free(specs, TRUE);
- return luaL_error(L, "invalid file entry (missing content)");
- }
- unsigned char *dupdata = NULL;
- if (slen > 0) {
- dupdata = g_malloc(slen);
- memcpy(dupdata, sdata, slen);
- }
- lua_pop(L, 1);
-
- lua_getfield(L, item_idx, "mode");
- if (lua_isnumber(L, -1)) {
- mode = (guint32) lua_tointeger(L, -1);
- }
- lua_pop(L, 1);
- lua_getfield(L, item_idx, "perms");
- if (lua_isnumber(L, -1)) {
- mode = (guint32) lua_tointeger(L, -1);
- }
- lua_pop(L, 1);
-
- lua_getfield(L, item_idx, "mtime");
- if (lua_isnumber(L, -1)) {
- mtime = (time_t) lua_tointeger(L, -1);
- }
- lua_pop(L, 1);
-
- struct rspamd_zip_file_spec s;
- s.name = dupname;
- s.data = dupdata;
- s.len = (gsize) slen;
- s.mtime = mtime;
- s.mode = mode;
- g_array_append_val(specs, s);
-
- lua_pop(L, 1);
+ /* Build args: ["zip", files, options] */
+ lua_settop(L, 1); /* keep only files */
+ if (password && *password) {
+ lua_newtable(L); /* options */
+ lua_pushstring(L, "password");
+ lua_pushstring(L, password);
+ lua_settable(L, -3);
+ /* options.zip = { encryption = "traditional" } */
+ lua_pushstring(L, "zip");
+ lua_newtable(L);
+ lua_pushstring(L, "encryption");
+ lua_pushstring(L, "traditional");
+ lua_settable(L, -3);
+ lua_settable(L, -3); /* options.zip = {...} */
}
-
- GByteArray *ba = rspamd_archives_zip_write((const struct rspamd_zip_file_spec *) specs->data,
- specs->len,
- password,
- &err);
-
- for (guint i = 0; i < specs->len; i++) {
- struct rspamd_zip_file_spec *s = &g_array_index(specs, struct rspamd_zip_file_spec, i);
- if (s->name) g_free((gpointer) s->name);
- if (s->data) g_free((gpointer) s->data);
+ else {
+ lua_pushnil(L); /* no options => plain ZIP */
}
- g_array_free(specs, TRUE);
- if (ba == NULL) {
- const char *emsg = (err && err->message) ? err->message : "zip encryption failed";
- if (err) g_error_free(err);
- return luaL_error(L, "%s", emsg);
- }
+ lua_pushstring(L, "zip");
+ lua_insert(L, 1); /* fmt at 1, files at 2, options/nil at 3 */
- size_t outlen = ba->len;
- guint8 *outdata = g_byte_array_free(ba, FALSE);
- struct rspamd_lua_text *txt = lua_new_text(L, (const char *) outdata, outlen, FALSE);
- txt->flags |= RSPAMD_TEXT_FLAG_OWN;
- return 1;
+ return lua_archive_pack(L);
}
static int
return luaL_error(L, "%s", lua_tostring(L, -1));
}
- /* Options (filters, etc.) at index 3 */
+ /* Options (filters, format options, password) at index 3 */
if (!lua_archive_add_filters(L, a, 3)) {
+ lua_pushstring(L, "cannot set compression filter(s)");
archive_write_free(a);
- return luaL_error(L, "cannot set compression filter(s)");
+ return lua_error(L);
+ }
+
+ if (!lua_archive_add_format_options(L, a, fmt, 3)) {
+ const char *aerr = archive_error_string(a);
+ lua_pushfstring(L, "cannot set format options: %s", aerr ? aerr : "unknown error");
+ archive_write_free(a);
+ return lua_error(L);
}
wctx.buf = g_byte_array_new();
* Unpacks an archive from a Lua string (or rspamd_text) using libarchive.
* @param {string|text} data archive contents
* @param {string} format optional format name to restrict autodetection (e.g. "zip")
- * @param {string} password optional password for encrypted archives (e.g. ZIP ZipCrypto)
+ * @param {string} password optional passphrase for encrypted archives (ZIP: ZipCrypto/AES)
* @return {table} array of files: { name = string, content = text } (non-regular entries are skipped)
*/
static int