Coverage Report

Created: 2026-03-12 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmArchiveWrite.cxx
Line
Count
Source
1
/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2
   file LICENSE.rst or https://cmake.org/licensing for details.  */
3
#include "cmArchiveWrite.h"
4
5
#include <cstdlib>
6
#include <ctime>
7
#include <iostream>
8
#include <limits>
9
#include <sstream>
10
#include <string>
11
#include <thread>
12
13
#include <cm/algorithm>
14
#include <cm/string_view>
15
16
#include <cm3p/archive.h>
17
#include <cm3p/archive_entry.h>
18
19
#include "cmsys/Directory.hxx"
20
#ifdef _WIN32
21
#  include "cmsys/Encoding.hxx"
22
#endif
23
#include "cmsys/FStream.hxx"
24
25
#include "cm_parse_date.h"
26
27
#include "cmStringAlgorithms.h"
28
#include "cmSystemTools.h"
29
30
#ifndef __LA_SSIZE_T
31
#  define __LA_SSIZE_T la_ssize_t
32
#endif
33
34
static std::string cm_archive_error_string(struct archive* a)
35
0
{
36
0
  char const* e = archive_error_string(a);
37
0
  return e ? e : "unknown error";
38
0
}
39
40
// Set path to be written to the archive.
41
static void cm_archive_entry_copy_pathname(struct archive_entry* e,
42
                                           char const* dest)
43
0
{
44
#ifdef _WIN32
45
  // libarchive converts our UTF-8 encoding to the archive's encoding.
46
  archive_entry_update_pathname_utf8(e, dest);
47
#else
48
  // libarchive converts our locale's encoding to the archive's encoding.
49
0
  archive_entry_copy_pathname(e, dest);
50
0
#endif
51
0
}
52
53
// Set path used for filesystem access.
54
static void cm_archive_entry_copy_sourcepath(struct archive_entry* e,
55
                                             std::string const& file)
56
0
{
57
#ifdef _WIN32
58
  archive_entry_copy_sourcepath_w(e, cmsys::Encoding::ToWide(file).c_str());
59
#else
60
0
  archive_entry_copy_sourcepath(e, file.c_str());
61
0
#endif
62
0
}
63
64
class cmArchiveWrite::Entry
65
{
66
  struct archive_entry* Object;
67
68
public:
69
  Entry()
70
0
    : Object(archive_entry_new())
71
0
  {
72
0
  }
73
0
  ~Entry() { archive_entry_free(this->Object); }
74
  Entry(Entry const&) = delete;
75
  Entry& operator=(Entry const&) = delete;
76
0
  operator struct archive_entry *() { return this->Object; }
77
};
78
79
struct cmArchiveWrite::Callback
80
{
81
  // archive_write_callback
82
  static __LA_SSIZE_T Write(struct archive* /*unused*/, void* cd,
83
                            void const* b, size_t n)
84
0
  {
85
0
    cmArchiveWrite* self = static_cast<cmArchiveWrite*>(cd);
86
0
    if (self->Stream.write(static_cast<char const*>(b),
87
0
                           static_cast<std::streamsize>(n))) {
88
0
      return static_cast<__LA_SSIZE_T>(n);
89
0
    }
90
0
    return static_cast<__LA_SSIZE_T>(-1);
91
0
  }
92
};
93
94
cmArchiveWrite::cmArchiveWrite(std::ostream& os, Compress c,
95
                               std::string const& format, int compressionLevel,
96
                               int numThreads)
97
0
  : Stream(os)
98
0
  , Archive(archive_write_new())
99
0
  , Disk(archive_read_disk_new())
100
0
  , Format(format)
101
0
{
102
  // Upstream fixed an issue with their integer parsing in 3.4.0
103
  // which would cause spurious errors to be raised from `strtoull`.
104
105
0
  if (archive_write_set_format_by_name(this->Archive, format.c_str()) !=
106
0
      ARCHIVE_OK) {
107
0
    this->Error = cmStrCat("archive_write_set_format_by_name: ",
108
0
                           cm_archive_error_string(this->Archive));
109
0
    return;
110
0
  }
111
112
0
  bool is7zip = (format == "7zip");
113
0
  bool isZip = (format == "zip");
114
0
  bool isFormatSupportsCompressionNatively = (is7zip || isZip);
115
116
0
  if (numThreads < 1) {
117
0
    int upperLimit = (numThreads == 0) ? std::numeric_limits<int>::max()
118
0
                                       : std::abs(numThreads);
119
120
0
    numThreads =
121
0
      cm::clamp<int>(std::thread::hardware_concurrency(), 1, upperLimit);
122
0
  }
123
124
0
  std::string sNumThreads = std::to_string(numThreads);
125
126
0
  if (!isFormatSupportsCompressionNatively) {
127
0
    switch (c) {
128
0
      case CompressNone:
129
0
        if (archive_write_add_filter_none(this->Archive) != ARCHIVE_OK) {
130
0
          this->Error = cmStrCat("archive_write_add_filter_none: ",
131
0
                                 cm_archive_error_string(this->Archive));
132
0
          return;
133
0
        }
134
0
        break;
135
0
      case CompressCompress:
136
0
        if (archive_write_add_filter_compress(this->Archive) != ARCHIVE_OK) {
137
0
          this->Error = cmStrCat("archive_write_add_filter_compress: ",
138
0
                                 cm_archive_error_string(this->Archive));
139
0
          return;
140
0
        }
141
0
        break;
142
0
      case CompressGZip: {
143
0
        if (archive_write_add_filter_gzip(this->Archive) != ARCHIVE_OK) {
144
0
          this->Error = cmStrCat("archive_write_add_filter_gzip: ",
145
0
                                 cm_archive_error_string(this->Archive));
146
0
          return;
147
0
        }
148
0
        std::string source_date_epoch;
149
0
        cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch);
150
0
        if (!source_date_epoch.empty()) {
151
          // We're not able to specify an arbitrary timestamp for gzip.
152
          // The next best thing is to omit the timestamp entirely.
153
0
          if (archive_write_set_filter_option(
154
0
                this->Archive, "gzip", "timestamp", nullptr) != ARCHIVE_OK) {
155
0
            this->Error = cmStrCat("archive_write_set_filter_option: ",
156
0
                                   cm_archive_error_string(this->Archive));
157
0
            return;
158
0
          }
159
0
        }
160
0
      } break;
161
0
      case CompressBZip2:
162
0
        if (archive_write_add_filter_bzip2(this->Archive) != ARCHIVE_OK) {
163
0
          this->Error = cmStrCat("archive_write_add_filter_bzip2: ",
164
0
                                 cm_archive_error_string(this->Archive));
165
0
          return;
166
0
        }
167
0
        break;
168
0
      case CompressLZMA:
169
0
        if (archive_write_add_filter_lzma(this->Archive) != ARCHIVE_OK) {
170
0
          this->Error = cmStrCat("archive_write_add_filter_lzma: ",
171
0
                                 cm_archive_error_string(this->Archive));
172
0
          return;
173
0
        }
174
0
        break;
175
0
      case CompressXZ:
176
0
        if (archive_write_add_filter_xz(this->Archive) != ARCHIVE_OK) {
177
0
          this->Error = cmStrCat("archive_write_add_filter_xz: ",
178
0
                                 cm_archive_error_string(this->Archive));
179
0
          return;
180
0
        }
181
182
0
#if ARCHIVE_VERSION_NUMBER >= 3004000
183
184
#  ifdef _AIX
185
        // FIXME: Using more than 2 threads creates an empty archive.
186
        // Enforce this limit pending further investigation.
187
        if (numThreads > 2) {
188
          numThreads = 2;
189
          sNumThreads = std::to_string(numThreads);
190
        }
191
#  endif
192
0
        if (archive_write_set_filter_option(this->Archive, "xz", "threads",
193
0
                                            sNumThreads.c_str()) !=
194
0
            ARCHIVE_OK) {
195
0
          this->Error = cmStrCat("archive_compressor_xz_options: ",
196
0
                                 cm_archive_error_string(this->Archive));
197
0
          return;
198
0
        }
199
0
#endif
200
201
0
        break;
202
0
      case CompressZstd:
203
0
        if (archive_write_add_filter_zstd(this->Archive) != ARCHIVE_OK) {
204
0
          this->Error = cmStrCat("archive_write_add_filter_zstd: ",
205
0
                                 cm_archive_error_string(this->Archive));
206
0
          return;
207
0
        }
208
209
0
#if ARCHIVE_VERSION_NUMBER >= 3006000
210
0
        if (archive_write_set_filter_option(this->Archive, "zstd", "threads",
211
0
                                            sNumThreads.c_str()) !=
212
0
            ARCHIVE_OK) {
213
0
          this->Error = cmStrCat("archive_compressor_zstd_options: ",
214
0
                                 cm_archive_error_string(this->Archive));
215
0
          return;
216
0
        }
217
0
#endif
218
0
        break;
219
0
      case CompressPPMd:
220
0
        this->Error = cmStrCat("PPMd is not supported for ", format);
221
0
        return;
222
0
    }
223
0
  }
224
225
0
  if (isFormatSupportsCompressionNatively || compressionLevel != 0) {
226
0
    std::string compressionLevelStr = std::to_string(compressionLevel);
227
0
    std::string archiveFilterName;
228
0
    switch (c) {
229
0
      case CompressNone:
230
0
        if (is7zip || isZip) {
231
0
          archiveFilterName = "store";
232
0
        } else {
233
          // Nothing to do - the value should be empty
234
0
        }
235
0
        break;
236
0
      case CompressCompress:
237
0
        if (is7zip || isZip) {
238
0
          this->Error =
239
0
            cmStrCat("CompressCompress is not supported for ", format);
240
0
        } else {
241
          // Nothing to do - the value should be empty
242
0
        }
243
0
        break;
244
0
      case CompressGZip:
245
0
        if (is7zip || isZip) {
246
0
          archiveFilterName = "deflate";
247
0
        } else {
248
0
          archiveFilterName = "gzip";
249
0
        }
250
0
        break;
251
0
      case CompressBZip2:
252
#if ARCHIVE_VERSION_NUMBER < 3008000
253
        if (isZip) {
254
          this->Error = cmStrCat("BZip2 is not supported for ", format,
255
                                 ". Please, build CMake with libarchive 3.8.0 "
256
                                 "or newer if you want to use it.");
257
          return;
258
        }
259
#endif
260
0
        archiveFilterName = "bzip2";
261
0
        break;
262
0
      case CompressLZMA:
263
#if ARCHIVE_VERSION_NUMBER < 3008000
264
        if (isZip) {
265
          this->Error = cmStrCat("LZMA is not supported for ", format,
266
                                 ". Please, build CMake with libarchive 3.8.0 "
267
                                 "or newer if you want to use it.");
268
          return;
269
        }
270
#endif
271
0
        if (is7zip) {
272
0
          archiveFilterName = "lzma1";
273
0
        } else {
274
0
          archiveFilterName = "lzma";
275
0
        }
276
0
        break;
277
0
      case CompressXZ:
278
#if ARCHIVE_VERSION_NUMBER < 3008000
279
        if (isZip) {
280
          this->Error = cmStrCat("LZMA2 (XZ) is not supported for ", format,
281
                                 ". Please, build CMake with libarchive 3.8.0 "
282
                                 "or newer if you want to use it.");
283
          return;
284
        }
285
#endif
286
0
        if (is7zip) {
287
0
          archiveFilterName = "lzma2";
288
0
        } else {
289
0
          archiveFilterName = "xz";
290
0
        }
291
0
        break;
292
0
      case CompressZstd:
293
#if ARCHIVE_VERSION_NUMBER < 3008000
294
        if (is7zip || isZip) {
295
          this->Error = cmStrCat("Zstd is not supported for ", format,
296
                                 ". Please, build CMake with libarchive 3.8.0 "
297
                                 "or newer if you want to use it.");
298
          return;
299
        }
300
#endif
301
0
        archiveFilterName = "zstd";
302
0
        break;
303
0
      case CompressPPMd:
304
0
        if (is7zip) {
305
0
          archiveFilterName = "ppmd";
306
0
        } else {
307
0
          this->Error = cmStrCat("PPMd is not supported for ", format);
308
0
        }
309
0
        return;
310
0
    }
311
312
0
    if (isFormatSupportsCompressionNatively) {
313
0
      if (archiveFilterName.empty()) {
314
0
        this->Error = cmStrCat("Unknown compression method for ", format);
315
0
        return;
316
0
      }
317
318
0
      if (archive_write_set_format_option(
319
0
            this->Archive, format.c_str(), "compression",
320
0
            archiveFilterName.c_str()) != ARCHIVE_OK) {
321
0
        this->Error =
322
0
          cmStrCat("archive_write_set_format_option(compression): ",
323
0
                   cm_archive_error_string(this->Archive));
324
0
        return;
325
0
      }
326
327
0
#if ARCHIVE_VERSION_NUMBER >= 3008000
328
0
      if (archive_write_set_format_option(this->Archive, format.c_str(),
329
0
                                          "threads",
330
0
                                          sNumThreads.c_str()) != ARCHIVE_OK) {
331
0
        this->Error = cmStrCat("archive_write_set_format_option(threads): ",
332
0
                               cm_archive_error_string(this->Archive));
333
0
        return;
334
0
      }
335
0
#endif
336
337
0
      if (compressionLevel != 0) {
338
0
        if (archive_write_set_format_option(
339
0
              this->Archive, format.c_str(), "compression-level",
340
0
              compressionLevelStr.c_str()) != ARCHIVE_OK) {
341
0
          this->Error =
342
0
            cmStrCat("archive_write_set_format_option(compression-level): ",
343
0
                     cm_archive_error_string(this->Archive));
344
0
          return;
345
0
        }
346
0
      }
347
0
    } else if (compressionLevel != 0 && !archiveFilterName.empty()) {
348
0
      if (archive_write_set_filter_option(
349
0
            this->Archive, archiveFilterName.c_str(), "compression-level",
350
0
            compressionLevelStr.c_str()) != ARCHIVE_OK) {
351
0
        this->Error = cmStrCat("archive_write_set_filter_option: ",
352
0
                               cm_archive_error_string(this->Archive));
353
0
        return;
354
0
      }
355
0
    }
356
0
  }
357
358
0
#if !defined(_WIN32) || defined(__CYGWIN__)
359
0
  if (archive_read_disk_set_standard_lookup(this->Disk) != ARCHIVE_OK) {
360
0
    this->Error = cmStrCat("archive_read_disk_set_standard_lookup: ",
361
0
                           cm_archive_error_string(this->Archive));
362
0
    return;
363
0
  }
364
0
#endif
365
366
  // do not pad the last block!!
367
0
  if (archive_write_set_bytes_in_last_block(this->Archive, 1)) {
368
0
    this->Error = cmStrCat("archive_write_set_bytes_in_last_block: ",
369
0
                           cm_archive_error_string(this->Archive));
370
0
    return;
371
0
  }
372
0
}
373
374
bool cmArchiveWrite::Open()
375
0
{
376
0
  if (!this->Error.empty()) {
377
0
    return false;
378
0
  }
379
0
  if (archive_write_open(
380
0
        this->Archive, this, nullptr,
381
0
        reinterpret_cast<archive_write_callback*>(&Callback::Write),
382
0
        nullptr) != ARCHIVE_OK) {
383
0
    this->Error =
384
0
      cmStrCat("archive_write_open: ", cm_archive_error_string(this->Archive));
385
0
    return false;
386
0
  }
387
0
  return true;
388
0
}
389
390
cmArchiveWrite::~cmArchiveWrite()
391
0
{
392
0
  archive_read_free(this->Disk);
393
0
  archive_write_free(this->Archive);
394
0
}
395
396
bool cmArchiveWrite::Add(std::string path, size_t skip, char const* prefix,
397
                         bool recursive)
398
0
{
399
0
  if (!path.empty() && path.back() == '/') {
400
0
    path.erase(path.size() - 1);
401
0
  }
402
0
  this->AddPath(path, skip, prefix, recursive);
403
0
  return this->Okay();
404
0
}
405
406
bool cmArchiveWrite::AddPath(std::string const& path, size_t skip,
407
                             char const* prefix, bool recursive)
408
0
{
409
0
  if (path != "." || (this->Format != "zip" && this->Format != "7zip")) {
410
0
    if (!this->AddFile(path, skip, prefix)) {
411
0
      return false;
412
0
    }
413
0
  }
414
0
  if ((!cmSystemTools::FileIsDirectory(path) || !recursive) ||
415
0
      cmSystemTools::FileIsSymlink(path)) {
416
0
    return true;
417
0
  }
418
0
  cmsys::Directory d;
419
0
  if (d.Load(path)) {
420
0
    std::string next = cmStrCat(path, '/');
421
0
    if (next == "./" && (this->Format == "zip" || this->Format == "7zip")) {
422
0
      next.clear();
423
0
    }
424
0
    std::string::size_type end = next.size();
425
0
    unsigned long n = d.GetNumberOfFiles();
426
0
    for (unsigned long i = 0; i < n; ++i) {
427
0
      std::string const& file = d.GetFileName(i);
428
0
      if (file != "." && file != "..") {
429
0
        next.erase(end);
430
0
        next += file;
431
0
        if (!this->AddPath(next, skip, prefix)) {
432
0
          return false;
433
0
        }
434
0
      }
435
0
    }
436
0
  }
437
0
  return true;
438
0
}
439
440
bool cmArchiveWrite::AddFile(std::string const& file, size_t skip,
441
                             char const* prefix)
442
0
{
443
0
  this->Error = "";
444
  // Skip the file if we have no name for it.  This may happen on a
445
  // top-level directory, which does not need to be included anyway.
446
0
  if (skip >= file.length()) {
447
0
    return true;
448
0
  }
449
0
  cm::string_view out = cm::string_view(file).substr(skip);
450
451
  // Meta-data.
452
0
  std::string dest = cmStrCat(prefix ? prefix : "", out);
453
0
  if (this->Verbose) {
454
0
    std::cout << dest << "\n";
455
0
  }
456
0
  Entry e;
457
0
  cm_archive_entry_copy_sourcepath(e, file);
458
0
  cm_archive_entry_copy_pathname(e, dest.c_str());
459
0
  if (archive_read_disk_entry_from_file(this->Disk, e, -1, nullptr) !=
460
0
      ARCHIVE_OK) {
461
0
    this->Error =
462
0
      cmStrCat("Unable to read from file:\n  ", file, "\nbecause:\n  ",
463
0
               cm_archive_error_string(this->Disk));
464
0
    return false;
465
0
  }
466
0
  if (!this->MTime.empty()) {
467
0
    time_t now;
468
0
    time(&now);
469
0
    time_t t = cm_parse_date(now, this->MTime.c_str());
470
0
    if (t == -1) {
471
0
      this->Error = cmStrCat("unable to parse mtime '", this->MTime, '\'');
472
0
      return false;
473
0
    }
474
0
    archive_entry_set_mtime(e, t, 0);
475
0
  } else {
476
0
    std::string source_date_epoch;
477
0
    cmSystemTools::GetEnv("SOURCE_DATE_EPOCH", source_date_epoch);
478
0
    if (!source_date_epoch.empty()) {
479
0
      std::istringstream iss(source_date_epoch);
480
0
      time_t epochTime;
481
0
      iss >> epochTime;
482
0
      if (iss.eof() && !iss.fail()) {
483
        // Set all of the file times to the epoch time to handle archive
484
        // formats that include creation/access time.
485
0
        archive_entry_set_mtime(e, epochTime, 0);
486
0
        archive_entry_set_atime(e, epochTime, 0);
487
0
        archive_entry_set_ctime(e, epochTime, 0);
488
0
        archive_entry_set_birthtime(e, epochTime, 0);
489
0
      }
490
0
    }
491
0
  }
492
493
  // manages the uid/guid of the entry (if any)
494
0
  if (this->Uid.IsSet() && this->Gid.IsSet()) {
495
0
    archive_entry_set_uid(e, this->Uid.Get());
496
0
    archive_entry_set_gid(e, this->Gid.Get());
497
0
  }
498
499
0
  if (!this->Uname.empty() && !this->Gname.empty()) {
500
0
    archive_entry_set_uname(e, this->Uname.c_str());
501
0
    archive_entry_set_gname(e, this->Gname.c_str());
502
0
  }
503
504
  // manages the permissions
505
0
  if (this->Permissions.IsSet()) {
506
0
    archive_entry_set_perm(e, this->Permissions.Get());
507
0
  }
508
509
0
  if (this->PermissionsMask.IsSet()) {
510
0
    int perm = archive_entry_perm(e);
511
0
    archive_entry_set_perm(e, perm & this->PermissionsMask.Get());
512
0
  }
513
514
  // Clear acl and xattr fields not useful for distribution.
515
0
  archive_entry_acl_clear(e);
516
0
  archive_entry_xattr_clear(e);
517
0
  archive_entry_set_fflags(e, 0, 0);
518
519
0
  if (this->Format == "pax" || this->Format == "paxr") {
520
    // Sparse files are a GNU tar extension.
521
    // Do not use them in standard tar files.
522
0
    archive_entry_sparse_clear(e);
523
0
  }
524
525
0
  if (archive_write_header(this->Archive, e) != ARCHIVE_OK) {
526
0
    this->Error = cmStrCat("archive_write_header: ",
527
0
                           cm_archive_error_string(this->Archive));
528
0
    return false;
529
0
  }
530
531
  // do not copy content of symlink
532
0
  if (!archive_entry_symlink(e)) {
533
    // Content.
534
0
    if (size_t size = static_cast<size_t>(archive_entry_size(e))) {
535
0
      return this->AddData(file, size);
536
0
    }
537
0
  }
538
0
  return true;
539
0
}
540
541
bool cmArchiveWrite::AddData(std::string const& file, size_t size)
542
0
{
543
0
  cmsys::ifstream fin(file.c_str(), std::ios::in | std::ios::binary);
544
0
  if (!fin) {
545
0
    this->Error = cmStrCat("Error opening \"", file,
546
0
                           "\": ", cmSystemTools::GetLastSystemError());
547
0
    return false;
548
0
  }
549
550
0
  char buffer[16384];
551
0
  size_t nleft = size;
552
0
  while (nleft > 0) {
553
0
    using ssize_type = std::streamsize;
554
0
    size_t const nnext = nleft > sizeof(buffer) ? sizeof(buffer) : nleft;
555
0
    ssize_type const nnext_s = static_cast<ssize_type>(nnext);
556
0
    fin.read(buffer, nnext_s);
557
    // Some stream libraries (older HPUX) return failure at end of
558
    // file on the last read even if some data were read.  Check
559
    // gcount instead of trusting the stream error status.
560
0
    if (static_cast<size_t>(fin.gcount()) != nnext) {
561
0
      break;
562
0
    }
563
0
    if (archive_write_data(this->Archive, buffer, nnext) != nnext_s) {
564
0
      this->Error = cmStrCat("archive_write_data: ",
565
0
                             cm_archive_error_string(this->Archive));
566
0
      return false;
567
0
    }
568
0
    nleft -= nnext;
569
0
  }
570
0
  if (nleft > 0) {
571
0
    this->Error = cmStrCat("Error reading \"", file,
572
0
                           "\": ", cmSystemTools::GetLastSystemError());
573
0
    return false;
574
0
  }
575
0
  return true;
576
0
}