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(int32_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 long 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(int32_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('/', 1);
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 == nullptr) {
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 == nullptr) {
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 == nullptr) {
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 == nullptr) {
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(static_cast<size_t>(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());
359 if (dirp == nullptr) {
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)) != nullptr ) {
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, static_cast<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 = static_cast<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 long fsize, const time_t ftime)
450 // add file info to index...
451 vpinfo.offset = static_cast<int32_t>(ftell(archive));
452 vpinfo.file_size = static_cast<int32_t>(fsize);
453 vpinfo.write_time = static_cast<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 == nullptr) {
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 != nullptr) {
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;
602 std::string rel_path;
604 size_t matching_count = 0;
606 for (it = m_index.begin(); it != m_index.end(); ++it) {
607 write_time = it->write_time;
608 rel_path = it->file_path;
610 rel_path.push_back('/');
611 rel_path.append(it->file_name);
613 if ( m_filtering && !std::regex_search(rel_path, match, m_regex) ) {
619 std::cout << std::setw(10) << it->file_size << " "
620 << std::put_time(std::gmtime(&write_time), "%F %H:%M:%S")
621 << " " << rel_path << std::endl;
624 std::cout << "---------- ---------- -------- ------------------------\n";
625 std::cout << std::setw(10) << m_header.index_offset-VP_HEADER_SIZE;
628 std::cout << " " << matching_count << " of " << m_index.size() << " file(s)\n";
630 std::cout << " " << m_index.size() << " file(s)\n";
633 std::cout << std::endl;
638 if (getAction() != vp::CREATE) {
644 if (m_filename.length() > 3) {
645 ext = m_filename.substr(m_filename.length()-3);
646 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
649 if ( ext.compare(".vp") ) {
650 m_filename.append(".vp");
653 std::cout << "VP file archiver/extractor - version 1.0\n";
654 std::cout << std::endl;
655 std::cout << "Creating archive '" << m_filename << "' ...\n";
656 std::cout << std::endl;
658 // TODO: verify directory
660 // write header place-holder
666 // update header with proper values
669 m_header.index_offset = static_cast<int32_t>(ftell(archive));
670 m_header.num_files = static_cast<int32_t>(m_index.size());
674 // finalize with file index and summary
677 size_t total_files = 0;
678 std::vector<vp_fileindex>::iterator it;
680 for (it = m_index.begin(); it != m_index.end(); ++it) {
681 if (it->file_size > 0) {
688 std::cout << std::endl;
689 std::cout << " Added " << total_files << " files\n";
690 std::cout << " Total size: " << ftell(archive) << " bytes\n";
691 std::cout << std::endl;
696 if (getAction() != vp::EXTRACT) {
703 std::cout << "VP file archiver/extractor - version 1.0\n";
704 std::cout << std::endl;
706 if ( !m_outdir.empty() ) {
707 std::cout << "Output directory: " << m_outdir << std::endl << std::endl;
710 std::cout << "Extracting '" << m_filename << "' ...\n";
712 const size_t BLOCK_SIZE = 1024*1024;
714 std::vector<vp_fileindex>::iterator it;
715 std::string path, rel_path;
717 int32_t bytes_remaining;
719 char data_block[BLOCK_SIZE];
723 for (it = m_index.begin(); it != m_index.end(); ++it) {
724 rel_path = it->file_path;
726 rel_path.push_back('/');
727 rel_path.append(it->file_name);
729 if ( m_filtering && !std::regex_search(rel_path, match, m_regex) ) {
733 std::cout << " " << rel_path << " ... ";
737 if ( !m_outdir.empty() ) {
741 path.append(rel_path);
743 if ( !CreatePath(path) ) {
744 std::cout << "ERROR: cannot create path!\n";
748 outfile = fopen(path.c_str(), "wb");
750 if (outfile == nullptr) {
751 std::cout << "ERROR: cannot create file!\n";
755 Seek(it->offset, SEEK_SET);
757 bytes_remaining = it->file_size;
759 while (bytes_remaining > 0) {
760 rval = fread(data_block, 1, std::min(BLOCK_SIZE, static_cast<size_t>(bytes_remaining)), archive);
763 fwrite(data_block, 1, rval, outfile);
764 bytes_remaining -= rval;
772 std::cout << "done!\n";
775 std::cout << std::endl;
776 std::cout << " " << ex_count << (m_filtering ? " matching" : "") << " files extracted\n";
777 std::cout << std::endl;
785 std::cout << "VP file archiver/extractor - version 1.0\n";
786 std::cout << std::endl;
787 std::cout << "Usage: cfileutil c <vp_filename> <source_dir>\n";
788 std::cout << " cfileutil x [-L] [-o <dir>] [-f <regex>] <vp_filename>\n";
789 std::cout << " cfileutil l [-f <regex>] <vp_filename>\n";
790 std::cout << std::endl;
791 std::cout << " Commands:\n";
792 std::cout << " c Create VP archive from <source_dir>\n";
793 std::cout << " x Extract all files into current directory\n";
794 std::cout << " l List all files in VP archive\n";
795 std::cout << std::endl;
796 std::cout << " Extraction options:\n";
797 std::cout << " -L Force all directory and file names to be lower case\n";
798 std::cout << " -o <dir> Extract into <dir> rather than current directory\n";
799 std::cout << " -f <regex> Only extract files matching regex\n";
800 std::cout << std::endl;
801 std::cout << " List options:\n";
802 std::cout << " -f <regex> Only list files matching regex\n";
803 std::cout << std::endl;
808 #define MAYBE_HELP(x) if (argc - idx < (x)) { help(); }
810 void parse_args(int argc, char *argv[])
812 for (int idx = 1; idx < argc; idx++) {
813 if ( !std::strcmp(argv[idx], "-h") || !std::strcmp(argv[idx], "--help") ) {
815 } else if ( !std::strcmp(argv[idx], "c") ) {
820 if (VP.getAction() != vp::INVALID) {
824 VP.setFilename(argv[idx++]);
825 VP.setSourceDirectory(argv[idx]);
826 VP.setAction(vp::CREATE);
827 } else if ( !std::strcmp(argv[idx], "l") ) {
832 if (VP.getAction() != vp::INVALID) {
836 for ( ; idx < argc; idx++) {
837 if ( !std::strcmp(argv[idx], "-f") ) {
842 VP.setRegex(argv[idx]);
844 VP.setFilename(argv[idx]);
845 VP.setAction(vp::LIST);
848 } else if ( !std::strcmp(argv[idx], "x") ) {
853 if (VP.getAction() != vp::INVALID) {
857 for ( ; idx < argc; idx++) {
858 if ( !std::strcmp(argv[idx], "-L") ) {
862 } else if ( !std::strcmp(argv[idx], "-o") ) {
867 VP.setOutputDirectory(argv[idx]);
868 } else if ( !std::strcmp(argv[idx], "-f") ) {
873 VP.setRegex(argv[idx]);
875 VP.setFilename(argv[idx]);
876 VP.setAction(vp::EXTRACT);
886 int main(int argc, char *argv[])
893 parse_args(argc, argv);
894 } catch (const std::regex_error &e) {
895 std::cerr << "Error validating regex => " << e.what() << std::endl;
897 } catch (const std::exception &e) {
898 std::cerr << "Error parsing arguments => " << e.what() << std::endl;
902 switch ( VP.getAction() ) {
906 } catch (const std::exception &e) {
907 std::cerr << "Error listing archive => " << e.what() << std::endl;
917 } catch (const std::exception &e) {
918 std::cerr << "Error creating archive => " << e.what() << std::endl;
928 } catch(const std::exception &e) {
929 std::cerr << "Error extracting archive => " << e.what() << std::endl;