2 * Copyright (C) Volition, Inc. 1999. All rights reserved.
4 * All source code herein is the property of Volition, Inc. You may not sell
5 * or otherwise commercially exploit the source or things you created based on
9 #define SDL_MAIN_HANDLED
11 #include "SDL_endian.h"
19 #include <sys/types.h>
32 #define CF_MAX_FILENAME_LENGTH 32 // Includes null terminater, so real length is 31
49 void setFilename(const char *val);
50 void setOutputDirectory(const char *val);
51 void setSourceDirectory(const char *val);
52 void setLowerCase() { m_lower_case = true; }
53 void setAction(action_t val) { m_action = val; }
54 void setRegex(const char *val);
56 action_t getAction() { return m_action; }
63 typedef struct vp_header {
64 int32_t id; // 0x50565056 : 'VPVP' (little endian)
66 int32_t index_offset; // offset where the file index starts and total file data size
67 int32_t num_files; // number of files, including each step in directory structure
70 typedef struct vp_fileindex {
75 std::string file_name;
77 // *not* part of VP, only used here
78 std::string file_path;
81 const int32_t VP_VERSION = 2;
82 const int32_t VP_ID = 0x50565056;
83 const int32_t VP_HEADER_SIZE = 16;
87 void Seek(size_t offset, int where);
89 char *ReadString(char *buf, const size_t length);
90 void WriteInt(const int32_t val);
91 void WriteString(const std::string &buf, const size_t length);
93 bool CreatePath(const std::string &path);
95 void pack_directory(const std::string *pack_dir = nullptr);
96 void add_directory(const std::string &a_dir);
97 void add_file(const std::string &a_file, const size_t fsize, const time_t ftime);
100 std::vector<vp_fileindex> m_index;
108 std::string m_filename;
109 std::string m_outdir;
110 std::string m_sourcedir;
118 void vp::Seek(size_t offset, int where)
120 int rval = fseek(archive, offset, where);
123 if (errno == EINVAL) {
124 throw std::runtime_error("invalid value in Seek()");
126 throw std::runtime_error("stream error in Seek()");
131 int32_t vp::ReadInt()
135 size_t rval = fread(&result, 1, sizeof(int32_t), archive);
137 if (rval != sizeof(int32_t)) {
138 throw std::runtime_error("short read in ReadInt()");
141 return SDL_SwapLE32(result);
144 char *vp::ReadString(char *buf, const size_t length)
146 size_t rval = fread(buf, 1, length, archive);
148 if (rval != length) {
149 throw std::runtime_error("short read in ReadString()");
155 void vp::WriteInt(const int32_t val)
157 int32_t value = SDL_SwapLE32(val);
159 size_t rval = fwrite(&value, 1, sizeof(int32_t), archive);
161 if (rval != sizeof(int32_t)) {
162 throw std::runtime_error("short write in WriteInt()");
166 void vp::WriteString(const std::string &buf, const size_t length)
168 if (buf.size() > length) {
169 throw std::runtime_error("string length greater than write length in WriteString()");
172 size_t rval = fwrite(buf.c_str(), 1, buf.size(), archive);
174 if (rval != buf.size()) {
175 throw std::runtime_error("short write in WriteString()");
178 // we need to write the full 'length', so zero fill any extra space
179 size_t fill = length - buf.size();
182 const char zero = '\0';
185 for (size_t idx = 0; idx < fill; idx++) {
186 rval += fwrite(&zero, 1, 1, archive);
190 throw std::runtime_error("short fill write in WriteString()");
195 bool vp::CreatePath(const std::string &path)
197 std::string sub_path;
198 std::string::size_type pos;
200 pos = path.find('/');
202 while (pos != std::string::npos) {
203 sub_path = path.substr(0, pos);
205 int status = mkdir(sub_path.c_str(), 0755);
207 if (status && (errno != EEXIST)) {
211 pos = path.find('/', pos+1);
217 void vp::read_header()
219 if (archive == NULL) {
220 if ( m_filename.empty() ) {
221 throw std::runtime_error("empty filename passed to vp::read_header()");
224 archive = fopen(m_filename.c_str(), "rb");
226 if (archive == NULL) {
228 std::ostringstream errmsg;
230 errmsg << "error opening '" << m_filename << "': " << std::strerror(err);
231 throw std::runtime_error(errmsg.str());
237 m_header.id = ReadInt();
238 m_header.version = ReadInt();
239 m_header.index_offset = ReadInt();
240 m_header.num_files = ReadInt();
242 if (m_header.id != vp::VP_ID) {
243 throw std::runtime_error("invalid VP ID");
246 if (m_header.version != vp::VP_VERSION) {
247 throw std::runtime_error("invalid VP version");
251 void vp::write_header()
253 if (archive == NULL) {
254 if ( m_filename.empty() ) {
255 throw std::runtime_error("empty filename passed to vp::write_header()");
258 archive = fopen(m_filename.c_str(), "wb");
260 if (archive == NULL) {
262 std::ostringstream errmsg;
264 errmsg << "error opening '" << m_filename << "': " << std::strerror(err);
265 throw std::runtime_error(errmsg.str());
272 WriteInt(vp::VP_VERSION);
273 WriteInt(m_header.index_offset);
274 WriteInt(m_header.num_files);
277 void vp::read_index()
279 Seek(m_header.index_offset, SEEK_SET);
282 char filename_tmp[CF_MAX_FILENAME_LENGTH];
285 memset(filename_tmp, 0, sizeof(filename_tmp));
287 // pre-allocate max size
288 m_index.reserve(m_header.num_files);
290 for (int i = 0; i < m_header.num_files; i++) {
291 vpinfo.offset = ReadInt();
292 vpinfo.file_size = ReadInt();
293 vpinfo.file_name = ReadString(filename_tmp, CF_MAX_FILENAME_LENGTH);
294 vpinfo.write_time = ReadInt();
297 std::transform(vpinfo.file_name.begin(), vpinfo.file_name.end(),
298 vpinfo.file_name.begin(), ::tolower);
301 // check if it's a directory and if so then create a path to use for files
302 if (vpinfo.file_size == 0) {
303 // if we get a ".." then drop down in the path
304 if ( !vpinfo.file_name.compare("..") ) {
305 std::string::size_type pos = path.find_last_of('/');
307 if (pos != std::string::npos) {
311 // otherwise add it to the path
313 if ( !path.empty() ) {
317 path.append(vpinfo.file_name);
320 // otherwise it's a file
322 vpinfo.file_path = path;
324 m_index.push_back(vpinfo);
329 void vp::write_index()
331 // index is appended to the end
334 std::vector<vp_fileindex>::iterator it;
336 for (it = m_index.begin(); it != m_index.end(); ++it) {
337 WriteInt(it->offset);
338 WriteInt(it->file_size);
339 WriteString(it->file_name, CF_MAX_FILENAME_LENGTH);
340 WriteInt(it->write_time);
344 void vp::pack_directory(const std::string *pack_dir)
349 std::string source_path, path;
351 if (pack_dir != nullptr) {
352 source_path = *pack_dir;
354 source_path = m_sourcedir;
357 dirp = opendir(source_path.c_str());
361 std::ostringstream errmsg;
363 errmsg << "error opening path '" << source_path << "': " << std::strerror(err);
364 throw std::runtime_error(errmsg.str());
367 std::cout << " Scanning '" << source_path << "' ...\n";
369 add_directory(source_path);
371 while ( (dir = readdir(dirp)) != NULL ) {
372 std::string name = dir->d_name;
374 if ( !name.compare(".") || !name.compare("..") ) {
378 if (name.length() >= CF_MAX_FILENAME_LENGTH) {
379 size_t half_len = std::min(name.length() / 2, (size_t)12);
380 std::string first_half = name.substr(0, half_len);
381 std::string last_half = name.substr(name.length() - half_len);
383 std::cout << " Skipping '" << source_path << "/" << first_half
384 << "..." << last_half << "' ... Name too long (> "
385 << CF_MAX_FILENAME_LENGTH-1 << " characters)\n";
387 } else if (name.length() > 2) {
388 std::string ext = name.substr(name.length()-3);
389 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
391 if ( !ext.compare(".vp") ) {
399 path.append(dir->d_name);
401 if ( stat(path.c_str(), &buf) == -1 ) {
405 if (buf.st_size > INT32_MAX) {
406 std::cout << " Skipping '" << source_path << "/" << name
407 << "' ... Size is too large (> " << INT32_MAX << " bytes)\n";
411 if ( S_ISDIR(buf.st_mode) ) {
412 // recurse into new directory
413 pack_directory(&path);
415 if (buf.st_size > 0) {
416 add_file(path, buf.st_size, buf.st_mtime);
423 // add a final directory root
424 add_directory(std::string(".."));
427 void vp::add_directory(const std::string &a_dir)
431 vpinfo.offset = (int32_t)ftell(archive);
432 vpinfo.file_size = 0;
433 vpinfo.write_time = 0;
434 vpinfo.file_name = a_dir;
436 // stip extra path from directory name
437 std::string::size_type pos = vpinfo.file_name.find_last_of('/');
439 if (pos != std::string::npos) {
440 vpinfo.file_name = vpinfo.file_name.substr(pos+1);
443 m_index.push_back(vpinfo);
446 void vp::add_file(const std::string &a_file, const size_t fsize, const time_t ftime)
450 // add file info to index...
451 vpinfo.offset = (int32_t)ftell(archive);
452 vpinfo.file_size = (int32_t)fsize;
453 vpinfo.write_time = (int32_t)ftime;
454 vpinfo.file_name = a_file;
456 // strip extra path from file name
457 std::string::size_type pos = vpinfo.file_name.find_last_of('/');
459 if (pos != std::string::npos) {
460 vpinfo.file_name = vpinfo.file_name.substr(pos+1);
463 m_index.push_back(vpinfo);
465 // add the file data to archive...
466 FILE *infile = fopen(a_file.c_str(), "rb");
468 if (infile == NULL) {
470 std::ostringstream errmsg;
472 errmsg << "error opening input file '" << a_file << "': " << std::strerror(err);
473 throw std::runtime_error(errmsg.str());
476 std::cout << " Adding '" << a_file << "' ... ";
478 const size_t BLOCK_SIZE = 1024*1024;
480 char data_block[BLOCK_SIZE];
481 size_t total_bytes = 0, num_bytes;
484 num_bytes = fread(data_block, 1, BLOCK_SIZE, infile);
487 fwrite(data_block, 1, num_bytes, archive);
488 total_bytes += num_bytes;
490 } while (num_bytes > 0);
494 std::cout << total_bytes << " bytes\n";
500 m_lower_case = false;
504 m_header.index_offset = VP_HEADER_SIZE; // size of vp_header (4 * sizeof(int32_t))
505 m_header.num_files = 0;
510 if (archive != NULL) {
516 void vp::setFilename(const char *val)
525 void vp::setOutputDirectory(const char *val)
533 if (m_outdir.back() == '/') {
538 void vp::setSourceDirectory(const char *val)
546 if (m_sourcedir.back() == '/') {
547 m_sourcedir.pop_back();
550 // make sure that it's named 'data', case insensitive
552 std::string::size_type pos = m_sourcedir.find_last_of('/');
554 if (pos != std::string::npos) {
555 is_data = m_sourcedir.substr(pos+1);
557 is_data = m_sourcedir;
560 std::transform(is_data.begin(), is_data.end(), is_data.begin(), ::tolower);
562 if (is_data.compare("data") ) {
563 throw std::runtime_error("source directory must be named 'data'");
567 void vp::setRegex(const char *val)
575 } catch (const std::regex_error &e) {
576 std::cout << "regex validation failure: " << e.what() << std::endl;
584 if (getAction() != vp::LIST) {
591 std::cout << "VP file archiver/extractor - version 1.0\n";
592 std::cout << std::endl;
593 std::cout << m_filename << " ...\n";
594 std::cout << std::endl;
596 std::cout << " Size Date Time Name\n";
597 std::cout << "---------- ---------- -------- ------------------------\n";
601 std::vector<vp_fileindex>::iterator it;
603 for (it = m_index.begin(); it != m_index.end(); ++it) {
604 write_time = it->write_time;
606 std::cout << std::setw(10) << it->file_size << " "
607 << std::put_time(std::gmtime(&write_time), "%F %H:%M:%S")
608 << " " << it->file_path << "/" << it->file_name << std::endl;
611 std::cout << "---------- ---------- -------- ------------------------\n";
612 std::cout << std::setw(10) << m_header.index_offset-VP_HEADER_SIZE
613 << " " << m_index.size() << " file(s)\n";
615 std::cout << std::endl;
620 if (getAction() != vp::CREATE) {
626 if (m_filename.length() > 3) {
627 ext = m_filename.substr(m_filename.length()-3);
628 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
631 if ( ext.compare(".vp") ) {
632 m_filename.append(".vp");
635 std::cout << "VP file archiver/extractor - version 1.0\n";
636 std::cout << std::endl;
637 std::cout << "Creating archive '" << m_filename << "' ...\n";
638 std::cout << std::endl;
640 // TODO: verify directory
642 // write header place-holder
648 // update header with proper values
651 m_header.index_offset = (int32_t)ftell(archive);
652 m_header.num_files = (int32_t)m_index.size();
656 // finalize with file index and summary
659 size_t total_files = 0;
660 std::vector<vp_fileindex>::iterator it;
662 for (it = m_index.begin(); it != m_index.end(); ++it) {
663 if (it->file_size > 0) {
670 std::cout << std::endl;
671 std::cout << " Added " << total_files << " files\n";
672 std::cout << " Total size: " << ftell(archive) << " bytes\n";
673 std::cout << std::endl;
678 if (getAction() != vp::EXTRACT) {
685 std::cout << "VP file archiver/extractor - version 1.0\n";
686 std::cout << std::endl;
688 if ( !m_outdir.empty() ) {
689 std::cout << "Output directory: " << m_outdir << std::endl << std::endl;
692 std::cout << "Extracting '" << m_filename << "' ...\n";
694 const size_t BLOCK_SIZE = 1024*1024;
696 std::vector<vp_fileindex>::iterator it;
697 std::string path, rel_path;
699 size_t bytes_remaining, rval;
700 char data_block[BLOCK_SIZE];
704 for (it = m_index.begin(); it != m_index.end(); ++it) {
705 rel_path = it->file_path;
707 rel_path.push_back('/');
708 rel_path.append(it->file_name);
710 if ( m_filtering && !std::regex_search(rel_path, match, m_regex) ) {
714 std::cout << " " << rel_path << " ... ";
718 if ( !m_outdir.empty() ) {
722 path.append(rel_path);
724 if ( !CreatePath(path) ) {
725 std::cout << "ERROR: cannot create path!\n";
729 outfile = fopen(path.c_str(), "wb");
731 if (outfile == NULL) {
732 std::cout << "ERROR: cannot create file!\n";
736 Seek(it->offset, SEEK_SET);
738 bytes_remaining = it->file_size;
740 while (bytes_remaining > 0) {
741 rval = fread(data_block, 1, std::min(BLOCK_SIZE, bytes_remaining), archive);
744 fwrite(data_block, 1, rval, outfile);
745 bytes_remaining -= rval;
753 std::cout << "done!\n";
756 std::cout << std::endl;
757 std::cout << " " << ex_count << (m_filtering ? " matching" : "") << " files extracted\n";
758 std::cout << std::endl;
766 std::cout << "VP file archiver/extractor - version 1.0\n";
767 std::cout << std::endl;
768 std::cout << "Usage: cfileutil -a <vp_filename> <source_dir>\n";
769 std::cout << " cfileutil -l <vp_filename>\n";
770 std::cout << " cfileutil -x [-L] [-o <dir>] [-f <regex>] <vp_filename>\n";
771 std::cout << std::endl;
772 std::cout << " Commands:\n";
773 std::cout << " -c Create VP archive from <source_dir>\n";
774 std::cout << " -x Extract all files into current directory\n";
775 std::cout << " -l List all files in VP archive\n";
776 std::cout << std::endl;
777 std::cout << " Extraction options:\n";
778 std::cout << " -L Force all directory and file names to be lower case\n";
779 std::cout << " -o <dir> Extract into <dir> rather than current directory\n";
780 std::cout << " -f <regex> Only extract files matching regex\n";
781 std::cout << std::endl;
786 #define MAYBE_HELP(x) if (argc - idx < (x)) { help(); }
788 void parse_args(int argc, char *argv[])
790 for (int idx = 1; idx < argc; idx++) {
791 if ( !std::strcmp(argv[idx], "-h") || !std::strcmp(argv[idx], "--help") ) {
793 } else if ( !std::strcmp(argv[idx], "-c") ) {
798 if (VP.getAction() != vp::INVALID) {
802 VP.setFilename(argv[idx++]);
803 VP.setSourceDirectory(argv[idx]);
804 VP.setAction(vp::CREATE);
805 } else if ( !std::strcmp(argv[idx], "-l") ) {
810 if (VP.getAction() != vp::INVALID) {
814 VP.setFilename(argv[idx]);
815 VP.setAction(vp::LIST);
816 } else if ( !std::strcmp(argv[idx], "-x") ) {
821 if (VP.getAction() != vp::INVALID) {
825 for ( ; idx < argc; idx++) {
826 if ( !std::strcmp(argv[idx], "-L") ) {
830 } else if ( !std::strcmp(argv[idx], "-o") ) {
835 VP.setOutputDirectory(argv[idx]);
836 } else if ( !std::strcmp(argv[idx], "-f") ) {
841 VP.setRegex(argv[idx]);
843 VP.setFilename(argv[idx]);
844 VP.setAction(vp::EXTRACT);
854 int main(int argc, char *argv[])
861 parse_args(argc, argv);
862 } catch (const std::regex_error &e) {
863 std::cerr << "Error validating regex => " << e.what() << std::endl;
865 } catch (const std::exception &e) {
866 std::cerr << "Error parsing arguments => " << e.what() << std::endl;
870 switch ( VP.getAction() ) {
874 } catch (const std::exception &e) {
875 std::cerr << "Error listing archive => " << e.what() << std::endl;
885 } catch (const std::exception &e) {
886 std::cerr << "Error creating archive => " << e.what() << std::endl;
896 } catch(const std::exception &e) {
897 std::cerr << "Error extracting archive => " << e.what() << std::endl;