15 #include "pandabase.h" 17 #include "panda_getopt.h" 18 #include "preprocess_argv.h" 19 #include "multifile.h" 20 #include "pointerTo.h" 23 #include "vector_string.h" 33 bool kill_cmd =
false;
35 bool compress_flag =
false;
36 int default_compression_level = 6;
38 bool got_multifile_name =
false;
39 bool to_stdout =
false;
40 bool encryption_flag =
false;
42 bool got_password =
false;
44 bool got_header_prefix =
false;
46 bool got_chdir_to =
false;
47 size_t scale_factor = 0;
50 vector_string sign_params;
53 string dont_compress_str =
"jpg,png,mp3,ogg";
56 string text_ext_str =
"txt";
58 bool got_record_timestamp_flag =
false;
59 bool record_timestamp_flag =
true;
74 string_to_int(
const string &str,
string &tail) {
75 const char *nptr = str.c_str();
77 int result = strtol(nptr, &endptr, 10);
89 string_to_int(
const string &str,
int &result) {
91 result = string_to_int(str, tail);
98 "Usage: multify -[c|r|u|t|x] -f <multifile_name> [options] <subfile_name> ...\n";
105 "multify is used to store and extract files from a Panda Multifile.\n" 106 "This is similar to a tar or zip file in that it is an archive file that\n" 107 "contains a number of subfiles that may later be extracted.\n\n" 109 "Panda's VirtualFileSystem is capable of mounting Multifiles for direct\n" 110 "access to the subfiles contained within without having to extract them\n" 111 "out to independent files first.\n\n" 113 "The command-line options for multify are designed to be similar to those\n" 114 "for tar, the traditional Unix archiver utility.\n\n" 118 " You must specify exactly one of the following command switches:\n\n" 121 " Create a new Multifile archive. Subfiles named on the command line\n" 122 " will be added to the new Multifile. If the Multifile already exists,\n" 123 " it is first removed.\n\n" 126 " Rewrite an existing Multifile archive. Subfiles named on the command\n" 127 " line will be added to the Multifile or will replace subfiles within\n" 128 " the Multifile with the same name. The Multifile will be repacked\n" 129 " after completion, even if no Subfiles were added.\n\n" 132 " Update an existing Multifile archive. This is similar to -r, except\n" 133 " that files are compared byte-for-byte with their corresponding files\n" 134 " in the archive first. If they have not changed, the multifile is not\n" 135 " modified (other than to repack it if necessary).\n\n" 138 " List the contents of an existing Multifile. With -v, this shows\n" 139 " the size of each Subfile and its compression ratio, if compressed.\n\n" 142 " Extract the contents of an existing Multifile. The Subfiles named on\n" 143 " the command line, or all Subfiles if nothing is named, are extracted\n" 144 " into the current directory or into whichever directory is specified\n" 148 " Delete (kill) the named Subfiles from the Multifile. The Multifile\n" 149 " will be repacked after completion.\n\n" 152 " You must always specify the following switch:\n\n" 154 " -f <multifile_name>\n" 155 " Names the Multifile that will be operated on.\n\n\n" 157 " The remaining switches are optional:\n\n" 160 " Run verbosely. In -c, -r, or -x mode, list each file as it is\n" 161 " written or extracted. In -t mode, list more information about each\n" 165 " Compress subfiles as they are written to the Multifile. Unlike tar\n" 166 " (but like zip), subfiles are compressed individually, instead of the\n" 167 " entire archive being compressed as one stream. It is not necessary\n" 168 " to specify -z when extracting compressed subfiles; they will always be\n" 169 " decompressed automatically. Also see -Z, which restricts which\n" 170 " subfiles will be compressed based on the filename extension.\n\n" 173 " Encrypt subfiles as they are written to the Multifile using the password\n" 174 " specified with -p, below. Subfiles are encrypted individually, rather\n" 175 " than encrypting the entire multifile, and different subfiles may be\n" 176 " encrypted using different passwords (although this requires running\n" 177 " multify multiple times). It is not possible to encrypt the multifile's\n" 178 " table of contents using this interface, but see the pencrypt program to\n" 179 " encrypt the entire multifile after it has been generated.\n\n" 183 " Specifies the password to encrypt or decrypt subfiles. If this is not\n" 184 " specified, and passwords are required, the user will be prompted from\n" 185 " standard input.\n\n" 188 " Specifies a header_prefix to write to the beginning of the multifile.\n" 189 " This is primarily useful for creating a multifile that can be invoked\n" 190 " directly as a program from the shell on Unix-like environments,\n" 191 " for instance, p3d files. The header_prefix must begin with a hash\n" 192 " mark and end with a newline; this will be enforced if it is not\n" 193 " already so. This only has effect in conjunction with with -c, -u,\n" 196 " -F <scale_factor>\n" 197 " Specify a Multifile scale factor. This is only necessary to support\n" 198 " Multifiles that will exceed 4GB in size. The default scale factor is\n" 199 " 1, which should be sufficient for almost any application, but the total\n" 200 " size of the Multifile will be limited to 4GB * scale_factor. The size\n" 201 " of individual subfiles may not exceed 4GB in any case.\n\n" 203 " -C <extract_dir>\n" 205 " Change to the named directory before working on files;\n" 206 " that is, extraction/creation/update and replace will be based on this path\n\n" 209 " With -x, extract subfiles to standard output instead of to disk.\n\n" 210 " -Z <extension_list>\n" 211 " Specify a comma-separated list of filename extensions that represent\n" 212 " files that are not to be compressed. The default if this is omitted is\n" 213 " \"" << dont_compress_str <<
"\". Specify -Z \"\" (be sure to include the space) to allow\n" 214 " all files to be compressed.\n\n" 215 " -X <extension_list>\n" 216 " Specify a comma-separated list of filename extensions that represent\n" 217 " text files. These files are opened and read in text mode, and added to\n" 218 " the multifile with the text flag set. The default if this is omitted is\n" 219 " \"" << text_ext_str <<
"\". Specify -X \"\" (be sure to include the space) to record\n" 220 " all files in binary mode.\n\n" 223 " Enable or disable the recording of file timestamps within the multifile.\n" 224 " If <flag> is 1, timestamps will be recorded within the multifile for\n" 225 " each subfile added; this is the default behavior. If <flag> is 0,\n" 226 " timestamps will not be recorded, which will make it easier to do a\n" 227 " bitwise comparison between multifiles to determine whether their\n" 228 " contents are equivalent.\n\n" 231 " Specify the compression level when -z is in effect. Larger numbers\n" 232 " generate slightly smaller files, but compression takes longer. The\n" 233 " default is -" << default_compression_level <<
".\n\n" 235 " -S file.crt[,chain.crt[,file.key[,\"password\"]]]\n" 236 " Sign the multifile. The signing certificate should be in PEM form in\n" 237 " file.crt, with its private key in PEM form in file.key. If the key\n" 238 " is encrypted on-disk, specify the decryption password as the third\n" 239 " option. If a certificate chain is required, chain.crt should also\n" 240 " be specified; note that the separating commas should be supplied\n" 241 " even if this optional filename is omitted.\n" 242 " You may also provide a single composite file that contains the\n" 243 " certificate, chain, and key in the same file.\n" 244 " PEM form is the form accepted by the Apache web server. The\n" 245 " signature is written to the multifile to prove it is unchanged; any\n" 246 " subsequent change to the multifile will invalidate the signature.\n" 247 " This parameter may be repeated to sign the multifile with additional\n" 248 " certificates.\n\n";
254 cerr <<
"Enter password: ";
255 getline(cin, password);
264 is_named(
const string &subfile_name,
const vector_string ¶ms) {
267 if (params.empty()) {
272 vector_string::const_iterator pi;
273 for (pi = params.begin(); pi != params.end(); ++pi) {
274 if (subfile_name == (*pi)) {
283 is_text(
const Filename &subfile_name) {
288 if (text_ext.find(ext) != text_ext.end()) {
297 get_compression_level(
const Filename &subfile_name) {
299 if (!compress_flag) {
305 if (dont_compress.find(ext) != dont_compress.end()) {
312 return default_compression_level;
322 cerr <<
"Unable to scan directory " << directory_name <<
"\n";
327 filenames.reserve(files.size());
328 vector_string::const_iterator fi;
329 for (fi = files.begin(); fi != files.end(); ++fi) {
330 Filename subfile_name(directory_name, (*fi));
331 filenames.push_back(subfile_name);
334 return do_add_files(multifile, filenames);
341 for (fi = filenames.begin(); fi != filenames.end(); ++fi) {
345 if (!do_add_directory(multifile, subfile_name)) {
349 }
else if (!subfile_name.
exists()) {
350 cerr <<
"Not found: " << subfile_name <<
"\n";
354 if (is_text(subfile_name)) {
360 string new_subfile_name;
363 (subfile_name, subfile_name, get_compression_level(subfile_name));
366 (subfile_name, subfile_name, get_compression_level(subfile_name));
368 if (new_subfile_name.empty()) {
369 cerr <<
"Unable to add " << subfile_name <<
".\n";
373 cout << new_subfile_name <<
"\n";
382 add_files(
const vector_string ¶ms) {
384 if (append || update) {
386 cerr <<
"Unable to open " << multifile_name <<
" for updating.\n";
391 cerr <<
"Unable to open " << multifile_name <<
" for writing.\n";
396 if (got_record_timestamp_flag) {
400 if (encryption_flag) {
405 if (got_header_prefix) {
410 cerr <<
"Setting scale factor to " << scale_factor <<
"\n";
415 filenames.reserve(params.size());
416 vector_string::const_iterator si;
417 for (si = params.begin(); si != params.end(); ++si) {
419 filenames.push_back(subfile_name);
423 if (got_chdir_to && !chdir_to.
chdir()) {
424 cout <<
"Failed to chdir to " << chdir_to <<
": " << strerror(errno) << endl;
428 bool okflag = do_add_files(multifile, filenames);
437 if (!multifile->
repack()) {
438 cerr <<
"Failed to write " << multifile_name <<
".\n";
442 if (!multifile->
flush()) {
443 cerr <<
"Failed to write " << multifile_name <<
".\n";
452 extract_files(
const vector_string ¶ms) {
453 if (!multifile_name.
exists()) {
454 cerr << multifile_name <<
" not found.\n";
458 if (!multifile->
open_read(multifile_name)) {
459 cerr <<
"Unable to open " << multifile_name <<
" for reading.\n";
469 bool any_encrypted =
false;
470 for (i = 0; i < num_subfiles && !any_encrypted; i++) {
472 if (is_named(subfile_name, params)) {
474 any_encrypted =
true;
484 for (i = 0; i < num_subfiles; i++) {
486 if (is_named(subfile_name, params)) {
489 filename =
Filename(chdir_to, subfile_name);
493 cerr << filename <<
"\n";
498 cout << filename <<
"\n";
509 kill_files(
const vector_string ¶ms) {
510 if (!multifile_name.
exists()) {
511 cerr << multifile_name <<
" not found.\n";
516 cerr <<
"Unable to open " << multifile_name <<
" for read/write.\n";
520 if (got_header_prefix) {
525 while (i < multifile->get_num_subfiles()) {
527 if (is_named(subfile_name, params)) {
531 cout << filename <<
"\n";
542 if (!multifile->
repack()) {
543 cerr <<
"Failed to write " << multifile_name <<
".\n";
547 if (!multifile->
flush()) {
548 cerr <<
"Failed to write " << multifile_name <<
".\n";
559 cerr <<
"Cannot sign multifiles without OpenSSL compiled in.\n";
562 #else // HAVE_OPENSSL 567 cerr <<
"Unable to re-open " << multifile_name <<
" for signing.\n";
571 vector_string::iterator si;
572 for (si = sign_params.begin(); si != sign_params.end(); ++si) {
573 const string ¶m = (*si);
577 size_t comma1 = param.find(
',');
578 if (comma1 == string::npos) {
582 size_t comma2 = param.find(
',', comma1 + 1);
583 if (comma2 == string::npos) {
587 size_t comma3 = param.find(
',', comma2 + 1);
588 if (comma3 == string::npos) {
592 password = param.substr(comma3 + 1);
597 if (!multifile->add_signature(certificate, chain, pkey, password)) {
603 #endif // HAVE_OPENSSL 607 format_timestamp(
bool record_timestamp, time_t timestamp) {
608 static const size_t buffer_size = 512;
609 static char buffer[buffer_size];
611 if (!record_timestamp) {
616 if (timestamp == 0) {
618 return " (no date) ";
621 time_t now = time(NULL);
622 struct tm *tm_p = localtime(×tamp);
624 if (timestamp > now || (now - timestamp > 86400 * 365)) {
627 strftime(buffer, buffer_size,
"%b %d %Y", tm_p);
630 strftime(buffer, buffer_size,
"%b %d %H:%M", tm_p);
637 list_files(
const vector_string ¶ms) {
638 if (!multifile_name.
exists()) {
639 cerr << multifile_name <<
" not found.\n";
643 if (!multifile->
open_read(multifile_name)) {
644 cerr <<
"Unable to open " << multifile_name <<
" for reading.\n";
652 cout << num_subfiles <<
" subfiles:\n" << flush;
653 for (i = 0; i < num_subfiles; i++) {
655 if (is_named(subfile_name, params)) {
656 char encrypted_symbol =
' ';
658 encrypted_symbol =
'e';
660 char text_symbol =
' ';
668 if (orig_length != 0) {
669 ratio = (double)internal_length / (
double)orig_length;
672 printf(
"%12d worse %c%c %s %s\n",
674 encrypted_symbol, text_symbol,
677 subfile_name.c_str());
679 printf(
"%12d %3.0f%% %c%c %s %s\n",
681 100.0 - ratio * 100.0,
682 encrypted_symbol, text_symbol,
685 subfile_name.c_str());
688 printf(
"%12d %c%c %s %s\n",
690 encrypted_symbol, text_symbol,
693 subfile_name.c_str());
700 cout <<
"Last modification " 701 << format_timestamp(
true, multifile->
get_timestamp()) <<
"\n";
708 cout <<
"Multifile needs to be repacked.\n";
711 for (i = 0; i < num_subfiles; i++) {
713 if (is_named(subfile_name, params)) {
714 cout << subfile_name <<
"\n";
720 int num_signatures = multifile->get_num_signatures();
721 if (num_signatures != 0) {
723 for (i = 0; i < num_signatures; ++i) {
724 cout <<
"Signed by " << multifile->get_signature_friendly_name(i);
725 int verify_result = multifile->validate_signature_certificate(i);
726 if (verify_result == 0) {
727 cout <<
" (certificate validated)\n";
729 cout <<
" (certificate unknown, reason " << verify_result <<
")\n";
732 multifile->write_signature_certificate(i, cout);
737 #endif // HAVE_OPENSSL 743 tokenize_extensions(
const string &str,
pset<string> &extensions) {
745 while (p < str.length()) {
746 size_t q = str.find_first_of(
",", p);
747 if (q == string::npos) {
748 extensions.insert(str.substr(p));
751 extensions.insert(str.substr(p, q - p));
754 extensions.insert(
string());
758 main(
int argc,
char **argv) {
762 preprocess_argv(argc, argv);
771 if (*argv[1] !=
'-' && *argv[1] !=
'\0') {
772 char *new_arg = (
char *)PANDA_MALLOC_ARRAY(strlen(argv[1]) + 2);
774 strcpy(new_arg + 1, argv[1]);
781 static const char *optflags =
"crutxkvz123456789Z:T:X:S:f:OC:ep:P:F:h";
782 int flag = getopt(argc, argv, optflags);
784 while (flag != EOF) {
808 compress_flag =
true;
811 default_compression_level = 1;
812 compress_flag =
true;
815 default_compression_level = 2;
816 compress_flag =
true;
819 default_compression_level = 3;
820 compress_flag =
true;
823 default_compression_level = 4;
824 compress_flag =
true;
827 default_compression_level = 5;
828 compress_flag =
true;
831 default_compression_level = 6;
832 compress_flag =
true;
835 default_compression_level = 7;
836 compress_flag =
true;
839 default_compression_level = 8;
840 compress_flag =
true;
843 default_compression_level = 9;
844 compress_flag =
true;
847 dont_compress_str = optarg;
850 text_ext_str = optarg;
853 sign_params.push_back(optarg);
858 if (!string_to_int(optarg, flag) ||
859 (flag != 0 && flag != 1)) {
860 cerr <<
"Invalid timestamp flag: " << optarg <<
"\n";
864 record_timestamp_flag = (flag != 0);
865 got_record_timestamp_flag =
true;
870 got_multifile_name =
true;
880 encryption_flag =
true;
887 header_prefix = optarg;
888 got_header_prefix =
true;
893 scale_factor = strtol(optarg, &endptr, 10);
894 if (*endptr !=
'\0') {
895 cerr <<
"Invalid integer: " << optarg <<
"\n";
899 if (scale_factor == 0) {
900 cerr <<
"Scale factor must be nonzero.\n";
914 cerr <<
"Unhandled switch: " << flag << endl;
917 flag = getopt(argc, argv, optflags);
919 argc -= (optind - 1);
920 argv += (optind - 1);
923 if ((create?1:0) + (append?1:0) + (update?1:0) + (tlist?1:0) + (extract?1:0) + (kill_cmd?1:0) != 1) {
924 cerr <<
"Exactly one of -c, -r, -u, -t, -x, -k must be specified.\n";
929 if (!got_multifile_name) {
930 cerr <<
"Multifile name not specified.\n";
936 tokenize_extensions(dont_compress_str, dont_compress);
939 tokenize_extensions(text_ext_str, text_ext);
942 vector_string params;
943 params.reserve(argc - 1);
944 for (
int i = 1; i < argc; i++) {
945 params.push_back(argv[i]);
949 if (create || append || update) {
950 okflag = add_files(params);
951 }
else if (extract) {
952 if (got_record_timestamp_flag) {
953 cerr <<
"Warning: -T ignored on extract.\n";
955 okflag = extract_files(params);
956 }
else if (kill_cmd) {
957 if (got_record_timestamp_flag) {
958 cerr <<
"Warning: -T ignored on kill.\n";
960 okflag = kill_files(params);
962 if (got_record_timestamp_flag) {
963 cerr <<
"Warning: -T ignored on list.\n";
965 okflag = list_files(params);
968 if (okflag && !sign_params.empty()) {
size_t get_scale_factor() const
Returns the internal scale factor for this Multifile.
void set_encryption_password(const string &encryption_password)
Specifies the password that will be used to encrypt subfiles subsequently added to the multifile...
size_t get_subfile_internal_length(int index) const
Returns the number of bytes the indicated subfile consumes within the archive.
bool open_write(const Filename &multifile_name)
Opens the named Multifile on disk for writing.
time_t get_timestamp() const
Returns the modification timestamp of the overall Multifile.
bool needs_repack() const
Returns true if the Multifile index is suboptimal and should be repacked.
void set_binary()
Indicates that the filename represents a binary file.
void set_text()
Indicates that the filename represents a text file.
bool is_subfile_compressed(int index) const
Returns true if the indicated subfile has been compressed when stored within the archive, false otherwise.
void set_header_prefix(const string &header_prefix)
Sets the string which is written to the Multifile before the Multifile header.
void set_encryption_flag(bool flag)
Sets the flag indicating whether subsequently-added subfiles should be encrypted before writing them ...
string get_extension() const
Returns the file extension.
bool is_subfile_encrypted(int index) const
Returns true if the indicated subfile has been encrypted when stored within the archive, false otherwise.
string update_subfile(const string &subfile_name, const Filename &filename, int compression_level)
Adds a file on disk to the subfile.
bool chdir() const
Changes directory to the specified location.
bool repack()
Forces a complete rewrite of the Multifile and all of its contents, so that its index will appear at ...
size_t get_subfile_length(int index) const
Returns the uncompressed data length of the nth subfile.
The name of a file, such as a texture file or an Egg file.
time_t get_subfile_timestamp(int index) const
Returns the modification time of the nth subfile.
void set_scale_factor(size_t scale_factor)
Changes the internal scale factor for this Multifile.
bool open_read_write(const Filename &multifile_name)
Opens the named Multifile on disk for reading and writing.
string add_subfile(const string &subfile_name, const Filename &filename, int compression_level)
Adds a file on disk as a subfile to the Multifile.
bool flush()
Writes all contents of the Multifile to disk.
bool get_record_timestamp() const
Returns the flag indicating whether timestamps should be recorded within the Multifile or not...
bool extract_subfile(int index, const Filename &filename)
Extracts the nth subfile into a file with the given name.
void set_record_timestamp(bool record_timestamp)
Sets the flag indicating whether timestamps should be recorded within the Multifile or not...
void remove_subfile(int index)
Removes the nth subfile from the Multifile.
bool extract_subfile_to(int index, ostream &out)
Extracts the nth subfile to the indicated ostream.
bool open_read(const Filename &multifile_name, const streampos &offset=0)
Opens the named Multifile on disk for reading.
bool is_directory() const
Returns true if the filename exists and is a directory name, false otherwise.
A file that contains a set of files.
bool scan_directory(vector_string &contents) const
Attempts to open the named filename as if it were a directory and looks for the non-hidden files with...
bool is_subfile_text(int index) const
Returns true if the indicated subfile represents text data, or false if it represents binary data...
const string & get_subfile_name(int index) const
Returns the name of the nth subfile.
bool exists() const
Returns true if the filename exists on the disk, false otherwise.
int get_num_subfiles() const
Returns the number of subfiles within the Multifile.
static Filename from_os_specific(const string &os_specific, Type type=T_general)
This named constructor returns a Panda-style filename (that is, using forward slashes, and no drive letter) based on the supplied filename string that describes a filename in the local system conventions (for instance, on Windows, it may use backslashes or begin with a drive letter and a colon).