open_toontown_panda3d/panda/src/express/multifile.cxx

2051 lines
70 KiB
C++

// Filename: multifile.cxx
// Created by: mike (09Jan97)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) Carnegie Mellon University. All rights reserved.
//
// All use of this software is subject to the terms of the revised BSD
// license. You should have received a copy of this license along
// with this source code in a file named "LICENSE."
//
////////////////////////////////////////////////////////////////////
#include "multifile.h"
#include "config_express.h"
#include "streamWriter.h"
#include "streamReader.h"
#include "datagram.h"
#include "zStream.h"
#include "encryptStream.h"
#include <algorithm>
// This sequence of bytes begins each Multifile to identify it as a
// Multifile.
const char Multifile::_header[] = "pmf\0\n\r";
const size_t Multifile::_header_size = 6;
// These numbers identify the version of the Multifile. Generally, a
// change in the major version is intolerable; while a Multifile with
// an older minor version may still be read.
const int Multifile::_current_major_ver = 1;
const int Multifile::_current_minor_ver = 1;
// Bumped to version 1.1 on 6/8/06 to add timestamps.
// To confirm that the supplied password matches, we write the
// Mutifile magic header at the beginning of the encrypted stream.
// I suppose this does compromise the encryption security a tiny
// bit by making it easy for crackers to validate that a
// particular password guess matches or doesn't match, but the
// encryption algorithm doesn't depend on this being difficult
// anyway.
const char Multifile::_encrypt_header[] = "crypty";
const size_t Multifile::_encrypt_header_size = 6;
//
// A Multifile consists of the following elements:
//
// (1) A header. This is always the first n bytes of the Multifile,
// and contains a magic number to identify the file, as well as
// version numbers and any file-specific parameters.
//
// char[6] The string Multifile::_header, a magic number.
// int16 The file's major version number
// int16 The file's minor version number
// uint32 Scale factor. This scales all address references within
// the file. Normally 1, this may be set larger to
// support Multifiles larger than 4GB.
// uint32 An overall modification timestamp for the entire multifile.
//
// (2) Zero or more index entries, one for each subfile within the
// Multifile. These entries are of variable length. The first one of
// these immediately follows the header, and the first word of each
// index entry contains the address of the next index entry. A zero
// "next" address marks the end of the chain. These may appear at any
// point within the Multifile; they do not necessarily appear in
// sequential order at the beginning of the file (although they will
// after the file has been "packed").
//
// uint32 The address of the next entry. 0 to mark the end.
// uint32 The address of this subfile's data record.
// uint32 The length in bytes of this subfile's data record.
// uint16 The Subfile::_flags member.
// [uint32] The original, uncompressed and unencrypted length of the
// subfile, if it is compressed or encrypted. This field
// is only present if one or both of the SF_compressed
// or SF_encrypted bits are set in _flags.
// uint32 A modification timestamp for the subfile.
// uint16 The length in bytes of the subfile's name.
// char[n] The subfile's name.
//
// (3) Zero or more data entries, one for each subfile. These may
// appear at any point within the Multifile; they do not necessarily
// follow each index entry, nor are they necessarily all grouped
// together at the end (although they will be all grouped together at
// the end after the file has been "packed"). These are just blocks
// of literal data.
//
////////////////////////////////////////////////////////////////////
// Function: Multifile::Constructor
// Access: Published
// Description:
////////////////////////////////////////////////////////////////////
Multifile::
Multifile() :
_read_filew(_read_file),
_read_write_filew(_read_write_file)
{
ConfigVariableInt multifile_encryption_iteration_count
("multifile-encryption-iteration-count", 0,
PRC_DESC("This is a special value of encryption-iteration-count used to encrypt "
"subfiles within a multifile. It has a default value of 0 (just one "
"application), on the assumption that the files from a multifile must "
"be loaded quickly, without paying the cost of an expensive hash on "
"each subfile in order to decrypt it."));
_read = (IStreamWrapper *)NULL;
_write = (ostream *)NULL;
_owns_stream = false;
_next_index = 0;
_last_index = 0;
_needs_repack = false;
_timestamp = 0;
_timestamp_dirty = false;
_record_timestamp = true;
_scale_factor = 1;
_new_scale_factor = 1;
_encryption_flag = false;
_encryption_iteration_count = multifile_encryption_iteration_count;
_file_major_ver = 0;
_file_minor_ver = 0;
#ifdef HAVE_OPENSSL
// Get these values from the config file via an EncryptStreamBuf.
EncryptStreamBuf tbuf;
_encryption_algorithm = tbuf.get_algorithm();
_encryption_key_length = tbuf.get_key_length();
#endif
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Destructor
// Access: Published
// Description:
////////////////////////////////////////////////////////////////////
Multifile::
~Multifile() {
close();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Copy Constructor
// Access: Private
// Description: Don't try to copy Multifiles.
////////////////////////////////////////////////////////////////////
Multifile::
Multifile(const Multifile &copy) :
_read_filew(_read_file),
_read_write_filew(_read_write_file)
{
nassertv(false);
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Copy Assignment Operator
// Access: Private
// Description: Don't try to copy Multifiles.
////////////////////////////////////////////////////////////////////
void Multifile::
operator = (const Multifile &copy) {
nassertv(false);
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_read
// Access: Published
// Description: Opens the named Multifile on disk for reading. The
// Multifile index is read in, and the list of subfiles
// becomes available; individual subfiles may then be
// extracted or read, but the list of subfiles may not
// be modified.
//
// Also see the version of open_read() which accepts an
// istream. Returns true on success, false on failure.
////////////////////////////////////////////////////////////////////
bool Multifile::
open_read(const Filename &multifile_name) {
close();
Filename fname = multifile_name;
fname.set_binary();
if (!fname.open_read(_read_file)) {
return false;
}
_timestamp = fname.get_timestamp();
_timestamp_dirty = true;
_read = &_read_filew;
_multifile_name = multifile_name;
return read_index();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_read
// Access: Public
// Description: Opens an anonymous Multifile for reading using an
// istream. There must be seek functionality via
// seekg() and tellg() on the istream.
//
// If owns_pointer is true, then the Multifile assumes
// ownership of the stream pointer and will delete it
// when the multifile is closed, including if this
// function returns false.
////////////////////////////////////////////////////////////////////
bool Multifile::
open_read(IStreamWrapper *multifile_stream, bool owns_pointer) {
close();
_timestamp = time(NULL);
_timestamp_dirty = true;
_read = multifile_stream;
_owns_stream = owns_pointer;
return read_index();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_write
// Access: Published
// Description: Opens the named Multifile on disk for writing. If
// there already exists a file by that name, it is
// truncated. The Multifile is then prepared for
// accepting a brand new set of subfiles, which will be
// written to the indicated filename. Individual
// subfiles may not be extracted or read.
//
// Also see the version of open_write() which accepts an
// ostream. Returns true on success, false on failure.
////////////////////////////////////////////////////////////////////
bool Multifile::
open_write(const Filename &multifile_name) {
close();
Filename fname = multifile_name;
fname.set_binary();
if (!fname.open_write(_write_file, true)) {
return false;
}
_timestamp = time(NULL);
_timestamp_dirty = true;
_write = &_write_file;
_multifile_name = multifile_name;
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_write
// Access: Public
// Description: Opens an anonymous Multifile for writing using an
// ostream. There must be seek functionality via
// seekp() and tellp() on the pstream.
//
// If owns_pointer is true, then the Multifile assumes
// ownership of the stream pointer and will delete it
// when the multifile is closed, including if this
// function returns false.
////////////////////////////////////////////////////////////////////
bool Multifile::
open_write(ostream *multifile_stream, bool owns_pointer) {
close();
_timestamp = time(NULL);
_timestamp_dirty = true;
_write = multifile_stream;
_owns_stream = owns_pointer;
_write->seekp(0, ios::beg);
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_read_write
// Access: Published
// Description: Opens the named Multifile on disk for reading and
// writing. If there already exists a file by that
// name, its index is read. Subfiles may be added or
// removed, and the resulting changes will be written to
// the named file.
//
// Also see the version of open_read_write() which
// accepts an iostream. Returns true on success, false
// on failure.
////////////////////////////////////////////////////////////////////
bool Multifile::
open_read_write(const Filename &multifile_name) {
close();
Filename fname = multifile_name;
fname.set_binary();
bool exists = fname.exists();
if (!fname.open_read_write(_read_write_file)) {
return false;
}
if (exists) {
_timestamp = fname.get_timestamp();
} else {
_timestamp = time(NULL);
}
_timestamp_dirty = true;
_read = &_read_write_filew;
_write = &_read_write_file;
_multifile_name = multifile_name;
if (exists) {
return read_index();
} else {
return true;
}
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_read_write
// Access: Public
// Description: Opens an anonymous Multifile for reading and writing
// using an iostream. There must be seek functionality
// via seekg()/seekp() and tellg()/tellp() on the
// iostream.
//
// If owns_pointer is true, then the Multifile assumes
// ownership of the stream pointer and will delete it
// when the multifile is closed, including if this
// function returns false.
////////////////////////////////////////////////////////////////////
bool Multifile::
open_read_write(iostream *multifile_stream, bool owns_pointer) {
close();
_timestamp = time(NULL);
_timestamp_dirty = true;
// We don't support locking when opening a file in read-write mode,
// because we don't bother with locking on write. But we need to
// have an IStreamWrapper to assign to the _read member, so we
// create one on-the-fly here.
_read = new StreamWrapper(multifile_stream, owns_pointer);
_write = multifile_stream;
_owns_stream = true; // Because we own the StreamWrapper, above.
_write->seekp(0, ios::beg);
// Check whether the read stream is empty.
multifile_stream->seekg(0, ios::end);
if (multifile_stream->tellg() == (streampos)0) {
// The read stream is empty, which is always valid.
return true;
}
// The read stream is not empty, so we'd better have a valid
// Multifile.
return read_index();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::close
// Access: Published
// Description: Closes the Multifile if it is open. All changes are
// flushed to disk, and the file becomes invalid for
// further operations until the next call to open().
////////////////////////////////////////////////////////////////////
void Multifile::
close() {
if (_new_scale_factor != _scale_factor) {
// If we have changed the scale factor recently, we need to force
// a repack.
repack();
} else {
flush();
}
if (_owns_stream) {
// We prefer to delete the IStreamWrapper over the ostream, if
// possible.
if (_read != (IStreamWrapper *)NULL) {
delete _read;
} else if (_write != (ostream *)NULL) {
delete _write;
}
}
_read = (IStreamWrapper *)NULL;
_write = (ostream *)NULL;
_owns_stream = false;
_next_index = 0;
_last_index = 0;
_needs_repack = false;
_timestamp = 0;
_timestamp_dirty = false;
_scale_factor = 1;
_new_scale_factor = 1;
_encryption_flag = false;
_file_major_ver = 0;
_file_minor_ver = 0;
_read_file.close();
_write_file.close();
_read_write_file.close();
_multifile_name = Filename();
clear_subfiles();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::set_scale_factor
// Access: Published
// Description: Changes the internal scale factor for this Multifile.
//
// This is normally 1, but it may be set to any
// arbitrary value (greater than zero) to support
// Multifile archives that exceed 4GB, if necessary.
// (Individual subfiles may still not exceed 4GB.)
//
// All addresses within the file are rounded up to the
// next multiple of _scale_factor, and zeros are written
// to the file to fill the resulting gaps. Then the
// address is divided by _scale_factor and written out
// as a 32-bit integer. Thus, setting a scale factor of
// 2 supports up to 8GB files, 3 supports 12GB files,
// etc.
//
// Calling this function on an already-existing
// Multifile will have no immediate effect until a
// future call to repack() or close() (or until the
// Multifile is destructed).
////////////////////////////////////////////////////////////////////
void Multifile::
set_scale_factor(size_t scale_factor) {
nassertv(is_write_valid());
nassertv(scale_factor != (size_t)0);
if (_next_index == (streampos)0) {
// If it's a brand new Multifile, we can go ahead and set it
// immediately.
_scale_factor = scale_factor;
} else {
// Otherwise, we'd better have read access so we can repack it
// later.
nassertv(is_read_valid());
}
// Setting the _new_scale_factor different from the _scale_factor
// will force a repack operation on close.
_new_scale_factor = scale_factor;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::add_subfile
// Access: Published
// Description: Adds a file on disk as a subfile to the Multifile.
// The file named by filename will be read and added to
// the Multifile at the next call to flush(). If there
// already exists a subfile with the indicated name, it
// is replaced without examining its contents (but see
// also update_subfile).
//
// Returns the subfile name on success (it might have
// been modified slightly), or empty string on failure.
////////////////////////////////////////////////////////////////////
string Multifile::
add_subfile(const string &subfile_name, const Filename &filename,
int compression_level) {
nassertr(is_write_valid(), string());
if (!filename.exists()) {
return string();
}
string name = standardize_subfile_name(subfile_name);
if (!name.empty()) {
Subfile *subfile = new Subfile;
subfile->_name = name;
subfile->_source_filename = filename;
subfile->_source_filename.set_binary();
add_new_subfile(subfile, compression_level);
}
_timestamp = time(NULL);
_timestamp_dirty = true;
return name;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::add_subfile
// Access: Public
// Description: Adds a file from a stream as a subfile to the Multifile.
// The indicated istream will be read and its contents
// added to the Multifile at the next call to flush().
//
// Note that the istream must remain untouched and
// unused by any other code until flush() is called. At
// that time, the Multifile will read the entire
// contents of the istream from the current file
// position to the end of the file. Subsequently, the
// Multifile will *not* close or delete the istream. It
// is the caller's responsibility to ensure that the
// istream pointer does not destruct during the lifetime
// of the Multifile.
//
// Returns the subfile name on success (it might have
// been modified slightly), or empty string on failure.
////////////////////////////////////////////////////////////////////
string Multifile::
add_subfile(const string &subfile_name, istream *subfile_data,
int compression_level) {
nassertr(is_write_valid(), string());
string name = standardize_subfile_name(subfile_name);
if (!name.empty()) {
Subfile *subfile = new Subfile;
subfile->_name = name;
subfile->_source = subfile_data;
add_new_subfile(subfile, compression_level);
}
return name;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::update_subfile
// Access: Published
// Description: Adds a file on disk to the subfile. If a subfile
// already exists with the same name, its contents are
// compared byte-for-byte to the disk file, and it is
// replaced only if it is different; otherwise, the
// multifile is left unchanged.
////////////////////////////////////////////////////////////////////
string Multifile::
update_subfile(const string &subfile_name, const Filename &filename,
int compression_level) {
nassertr(is_write_valid(), string());
if (!filename.exists()) {
return string();
}
string name = standardize_subfile_name(subfile_name);
if (!name.empty()) {
int index = find_subfile(name);
if (index >= 0) {
// The subfile already exists; compare it to the source file.
if (compare_subfile(index, filename)) {
// The files are identical; do nothing.
return name;
}
}
// The subfile does not already exist or it is different from the
// source file. Add the new source file.
Subfile *subfile = new Subfile;
subfile->_name = name;
subfile->_source_filename = filename;
subfile->_source_filename.set_binary();
add_new_subfile(subfile, compression_level);
}
_timestamp = time(NULL);
_timestamp_dirty = true;
return name;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::flush
// Access: Published
// Description: Writes all contents of the Multifile to disk. Until
// flush() is called, add_subfile() and remove_subfile()
// do not actually do anything to disk. At this point,
// all of the recently-added subfiles are read and their
// contents are added to the end of the Multifile, and
// the recently-removed subfiles are marked gone from
// the Multifile.
//
// This may result in a suboptimal index. To guarantee
// that the index is written at the beginning of the
// file, call repack() instead of flush().
//
// It is not necessary to call flush() explicitly unless
// you are concerned about reading the recently-added
// subfiles immediately.
//
// Returns true on success, false on failure.
////////////////////////////////////////////////////////////////////
bool Multifile::
flush() {
if (!is_write_valid()) {
return false;
}
if (_next_index == (streampos)0) {
// If we don't have an index yet, we don't have a header. Write
// the header.
if (!write_header()) {
return false;
}
} else {
if (_file_minor_ver != _current_minor_ver) {
// If we *do* have an index already, but this is an old version
// multifile, we have to completely rewrite it anyway.
return repack();
}
}
nassertr(_write != (ostream *)NULL, false);
// First, mark out all of the removed subfiles.
PendingSubfiles::iterator pi;
for (pi = _removed_subfiles.begin(); pi != _removed_subfiles.end(); ++pi) {
Subfile *subfile = (*pi);
subfile->rewrite_index_flags(*_write);
delete subfile;
}
_removed_subfiles.clear();
bool wrote_ok = true;
if (!_new_subfiles.empty()) {
// Add a few more files to the end. We always add subfiles at the
// end of the multifile, so go there first.
sort(_new_subfiles.begin(), _new_subfiles.end(), IndirectLess<Subfile>());
if (_last_index != (streampos)0) {
_write->seekp(0, ios::end);
if (_write->fail()) {
express_cat.info()
<< "Unable to seek Multifile " << _multifile_name << ".\n";
return false;
}
_next_index = _write->tellp();
_next_index = pad_to_streampos(_next_index);
// And update the forward link from the last_index to point to
// this new index location.
_write->seekp(_last_index);
StreamWriter writer(_write, false);
writer.add_uint32(streampos_to_word(_next_index));
}
_write->seekp(_next_index);
nassertr(_next_index == _write->tellp(), false);
// Ok, here we are at the end of the file. Write out the
// recently-added subfiles here. First, count up the index size.
for (pi = _new_subfiles.begin(); pi != _new_subfiles.end(); ++pi) {
Subfile *subfile = (*pi);
_last_index = _next_index;
_next_index = subfile->write_index(*_write, _next_index, this);
nassertr(_next_index == _write->tellp(), false);
_next_index = pad_to_streampos(_next_index);
nassertr(_next_index == _write->tellp(), false);
}
// Now we're at the end of the index. Write a 0 here to mark the
// end.
StreamWriter writer(_write, false);
writer.add_uint32(0);
_next_index += 4;
nassertr(_next_index == _write->tellp(), false);
_next_index = pad_to_streampos(_next_index);
// All right, now write out each subfile's data.
for (pi = _new_subfiles.begin(); pi != _new_subfiles.end(); ++pi) {
Subfile *subfile = (*pi);
if (_read != (IStreamWrapper *)NULL) {
_read->acquire();
_next_index = subfile->write_data(*_write, _read->get_istream(),
_next_index, this);
_read->release();
} else {
_next_index = subfile->write_data(*_write, NULL, _next_index, this);
}
nassertr(_next_index == _write->tellp(), false);
_next_index = pad_to_streampos(_next_index);
if (subfile->is_data_invalid()) {
wrote_ok = false;
}
nassertr(_next_index == _write->tellp(), false);
}
// Now go back and fill in the proper addresses for the data start.
// We didn't do it in the first pass, because we don't really want
// to keep all those file handles open, and so we didn't have to
// determine each file's length ahead of time.
for (pi = _new_subfiles.begin(); pi != _new_subfiles.end(); ++pi) {
Subfile *subfile = (*pi);
subfile->rewrite_index_data_start(*_write, this);
}
_new_subfiles.clear();
}
// Also update the overall timestamp.
if (_timestamp_dirty) {
nassertr(!_write->fail(), false);
static const size_t timestamp_pos = _header_size + 2 + 2 + 4;
_write->seekp(timestamp_pos);
nassertr(!_write->fail(), false);
StreamWriter writer(*_write);
if (_record_timestamp) {
writer.add_uint32(_timestamp);
} else {
writer.add_uint32(0);
}
_timestamp_dirty = false;
}
_write->flush();
if (!wrote_ok || _write->fail()) {
express_cat.info()
<< "Unable to update Multifile " << _multifile_name << ".\n";
close();
return false;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::repack
// Access: Published
// Description: Forces a complete rewrite of the Multifile and all of
// its contents, so that its index will appear at the
// beginning of the file with all of the subfiles listed
// in alphabetical order. This is considered optimal
// for reading, and is the standard configuration; but
// it is not essential to do this.
//
// It is only valid to call this if the Multifile was
// opened using open_read_write() and an explicit
// filename, rather than an iostream. Also, we must
// have write permission to the directory containing the
// Multifile.
//
// Returns true on success, false on failure.
////////////////////////////////////////////////////////////////////
bool Multifile::
repack() {
if (_next_index == (streampos)0) {
// If the Multifile hasn't yet been written, this is really just a
// flush operation.
_needs_repack = false;
return flush();
}
nassertr(is_write_valid() && is_read_valid(), false);
nassertr(!_multifile_name.empty(), false);
// First, we open a temporary filename to copy the Multifile to.
Filename dirname = _multifile_name.get_dirname();
if (dirname.empty()) {
dirname = ".";
}
Filename temp_filename = Filename::temporary(dirname, "mftemp");
temp_filename.set_binary();
pofstream temp;
if (!temp_filename.open_write(temp)) {
express_cat.info()
<< "Unable to open temporary file " << temp_filename << "\n";
return false;
}
// Now we scrub our internal structures so it looks like we're a
// brand new Multifile.
PendingSubfiles::iterator pi;
for (pi = _removed_subfiles.begin(); pi != _removed_subfiles.end(); ++pi) {
Subfile *subfile = (*pi);
delete subfile;
}
_removed_subfiles.clear();
_new_subfiles.clear();
copy(_subfiles.begin(), _subfiles.end(), back_inserter(_new_subfiles));
_next_index = 0;
_last_index = 0;
_scale_factor = _new_scale_factor;
// And we write our contents to our new temporary file.
_write = &temp;
if (!flush()) {
temp.close();
temp_filename.unlink();
return false;
}
// Now close everything, and move the temporary file back over our
// original file.
Filename orig_name = _multifile_name;
temp.close();
close();
orig_name.unlink();
if (!temp_filename.rename_to(orig_name)) {
express_cat.info()
<< "Unable to rename temporary file " << temp_filename << " to "
<< orig_name << ".\n";
return false;
}
if (!open_read_write(orig_name)) {
express_cat.info()
<< "Unable to read newly repacked " << _multifile_name
<< ".\n";
return false;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_num_subfiles
// Access: Published
// Description: Returns the number of subfiles within the Multifile.
// The subfiles may be accessed in alphabetical order by
// iterating through [0 .. get_num_subfiles()).
////////////////////////////////////////////////////////////////////
int Multifile::
get_num_subfiles() const {
return _subfiles.size();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::find_subfile
// Access: Published
// Description: Returns the index of the subfile with the indicated
// name, or -1 if the named subfile is not within the
// Multifile.
////////////////////////////////////////////////////////////////////
int Multifile::
find_subfile(const string &subfile_name) const {
Subfile find_subfile;
find_subfile._name = standardize_subfile_name(subfile_name);
Subfiles::const_iterator fi;
fi = _subfiles.find(&find_subfile);
if (fi == _subfiles.end()) {
// Not present.
return -1;
}
return (fi - _subfiles.begin());
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::has_directory
// Access: Published
// Description: Returns true if the indicated subfile name is the
// directory prefix to one or more files within the
// Multifile. That is, the Multifile contains at least
// one file named "subfile_name/...".
////////////////////////////////////////////////////////////////////
bool Multifile::
has_directory(const string &subfile_name) const {
string prefix = subfile_name;
if (!prefix.empty()) {
prefix += '/';
}
Subfile find_subfile;
find_subfile._name = prefix;
Subfiles::const_iterator fi;
fi = _subfiles.upper_bound(&find_subfile);
if (fi == _subfiles.end()) {
// Not present.
return false;
}
// At least one subfile exists whose name sorts after prefix. If it
// contains prefix as the initial substring, then we have a match.
Subfile *subfile = (*fi);
return (subfile->_name.length() > prefix.length() &&
subfile->_name.substr(0, prefix.length()) == prefix);
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::scan_directory
// Access: Published
// Description: Considers subfile_name to be the name of a
// subdirectory within the Multifile, but not a file
// itself; fills the given vector up with the sorted list
// of subdirectories or files within the named
// directory.
//
// Note that directories do not exist explicitly within
// a Multifile; this just checks for the existence of
// files with the given initial prefix.
//
// Returns true if successful, false otherwise.
////////////////////////////////////////////////////////////////////
bool Multifile::
scan_directory(vector_string &contents, const string &subfile_name) const {
string prefix = subfile_name;
if (!prefix.empty()) {
prefix += '/';
}
Subfile find_subfile;
find_subfile._name = prefix;
Subfiles::const_iterator fi;
fi = _subfiles.upper_bound(&find_subfile);
string previous = "";
while (fi != _subfiles.end()) {
Subfile *subfile = (*fi);
if (!(subfile->_name.length() > prefix.length() &&
subfile->_name.substr(0, prefix.length()) == prefix)) {
// We've reached the end of the list of subfiles beneath the
// indicated directory prefix.
return true;
}
size_t slash = subfile->_name.find('/', prefix.length());
string basename = subfile->_name.substr(prefix.length(), slash - prefix.length());
if (basename != previous) {
contents.push_back(basename);
previous = basename;
}
++fi;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::remove_subfile
// Access: Published
// Description: Removes the nth subfile from the Multifile. This
// will cause all subsequent index numbers to decrease
// by one. The file will not actually be removed from
// the disk until the next call to flush().
//
// Note that this does not actually remove the data from
// the indicated subfile; it simply removes it from the
// index. The Multifile will not be reduced in size
// after this operation, until the next call to
// repack().
////////////////////////////////////////////////////////////////////
void Multifile::
remove_subfile(int index) {
nassertv(is_write_valid());
nassertv(index >= 0 && index < (int)_subfiles.size());
Subfile *subfile = _subfiles[index];
subfile->_flags |= SF_deleted;
_removed_subfiles.push_back(subfile);
_subfiles.erase(_subfiles.begin() + index);
_timestamp = time(NULL);
_timestamp_dirty = true;
_needs_repack = true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_subfile_name
// Access: Published
// Description: Returns the name of the nth subfile.
////////////////////////////////////////////////////////////////////
const string &Multifile::
get_subfile_name(int index) const {
#ifndef NDEBUG
static string empty_string;
nassertr(index >= 0 && index < (int)_subfiles.size(), empty_string);
#endif
return _subfiles[index]->_name;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_subfile_length
// Access: Published
// Description: Returns the uncompressed data length of the nth
// subfile. This might return 0 if the subfile has
// recently been added and flush() has not yet been
// called.
////////////////////////////////////////////////////////////////////
size_t Multifile::
get_subfile_length(int index) const {
nassertr(index >= 0 && index < (int)_subfiles.size(), 0);
return _subfiles[index]->_uncompressed_length;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_subfile_timestamp
// Access: Published
// Description: Returns the modification time of the nth
// subfile. If this is called on an older .mf file,
// which did not store individual timestamps in the file
// (or if get_record_timestamp() is false), this will
// return the modification time of the overall
// multifile.
////////////////////////////////////////////////////////////////////
time_t Multifile::
get_subfile_timestamp(int index) const {
nassertr(index >= 0 && index < (int)_subfiles.size(), 0);
if (!get_record_timestamp()) {
return get_timestamp();
} else {
return _subfiles[index]->_timestamp;
}
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::is_subfile_compressed
// Access: Published
// Description: Returns true if the indicated subfile has been
// compressed when stored within the archive, false
// otherwise.
////////////////////////////////////////////////////////////////////
bool Multifile::
is_subfile_compressed(int index) const {
nassertr(index >= 0 && index < (int)_subfiles.size(), 0);
return (_subfiles[index]->_flags & SF_compressed) != 0;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::is_subfile_encrypted
// Access: Published
// Description: Returns true if the indicated subfile has been
// encrypted when stored within the archive, false
// otherwise.
////////////////////////////////////////////////////////////////////
bool Multifile::
is_subfile_encrypted(int index) const {
nassertr(index >= 0 && index < (int)_subfiles.size(), 0);
return (_subfiles[index]->_flags & SF_encrypted) != 0;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_index_end
// Access: Published
// Description: Returns the first byte that is guaranteed to follow
// any index byte already written to disk in the
// Multifile.
//
// This number is largely meaningless in many cases, but
// if needs_repack() is false, and the file is flushed,
// this will indicate the number of bytes in the header
// + index. Everything at this byte position and later
// will be actual data.
////////////////////////////////////////////////////////////////////
streampos Multifile::
get_index_end() const {
return normalize_streampos(_next_index + (streampos)4);
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_subfile_internal_start
// Access: Published
// Description: Returns the starting byte position within the
// Multifile at which the indicated subfile begins.
// This may be used, with get_subfile_internal_length(),
// for low-level access to the subfile, but usually it
// is better to use open_read_subfile() instead (which
// automatically decrypts and/or uncompresses the
// subfile data).
////////////////////////////////////////////////////////////////////
streampos Multifile::
get_subfile_internal_start(int index) const {
nassertr(index >= 0 && index < (int)_subfiles.size(), 0);
return _subfiles[index]->_data_start;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::get_subfile_internal_length
// Access: Published
// Description: Returns the number of bytes the indicated subfile
// consumes within the archive. For compressed
// subfiles, this will generally be smaller than
// get_subfile_length(); for encrypted (but
// noncompressed) subfiles, it may be slightly
// different, for noncompressed and nonencrypted
// subfiles, it will be equal.
////////////////////////////////////////////////////////////////////
size_t Multifile::
get_subfile_internal_length(int index) const {
nassertr(index >= 0 && index < (int)_subfiles.size(), 0);
return _subfiles[index]->_data_length;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::open_read_subfile
// Access: Published
// Description: Returns an istream that may be used to read the
// indicated subfile. You may seek() within this
// istream to your heart's content; even though it will
// be a reference to the already-opened pfstream of the
// Multifile itself, byte 0 appears to be the beginning
// of the subfile and EOF appears to be the end of the
// subfile.
//
// The returned istream will have been allocated via
// new; you should pass the pointer to
// close_read_subfile() when you are finished with it to
// delete it and release its resources.
//
// Any future calls to repack() or close() (or the
// Multifile destructor) will invalidate all currently
// open subfile pointers.
//
// The return value will be NULL if the stream cannot be
// opened for some reason.
////////////////////////////////////////////////////////////////////
istream *Multifile::
open_read_subfile(int index) {
nassertr(is_read_valid(), NULL);
nassertr(index >= 0 && index < (int)_subfiles.size(), NULL);
Subfile *subfile = _subfiles[index];
if (subfile->_source != (istream *)NULL ||
!subfile->_source_filename.empty()) {
// The subfile has not yet been copied into the physical
// Multifile. Force a flush operation to incorporate it.
flush();
// That shouldn't change the subfile index or delete the subfile
// pointer.
nassertr(subfile == _subfiles[index], NULL);
}
// Return an ISubStream object that references into the open
// Multifile istream.
nassertr(subfile->_data_start != (streampos)0, NULL);
istream *stream =
new ISubStream(_read, subfile->_data_start,
subfile->_data_start + (streampos)subfile->_data_length);
if ((subfile->_flags & SF_encrypted) != 0) {
#ifndef HAVE_OPENSSL
express_cat.error()
<< "OpenSSL not compiled in; cannot read encrypted multifiles.\n";
delete stream;
return NULL;
#else // HAVE_OPENSSL
// The subfile is encrypted. So actually, return an
// IDecryptStream that wraps around the ISubStream.
IDecryptStream *wrapper =
new IDecryptStream(stream, true, _encryption_password);
stream = wrapper;
// Validate the password by confirming that the encryption header
// matches.
char this_header[_encrypt_header_size];
stream->read(this_header, _encrypt_header_size);
if (stream->fail() || stream->gcount() != (unsigned)_encrypt_header_size ||
memcmp(this_header, _encrypt_header, _encrypt_header_size) != 0) {
express_cat.error()
<< "Unable to decrypt subfile " << subfile->_name << ".\n";
delete stream;
return NULL;
}
#endif // HAVE_OPENSSL
}
if ((subfile->_flags & SF_compressed) != 0) {
#ifndef HAVE_ZLIB
express_cat.error()
<< "zlib not compiled in; cannot read compressed multifiles.\n";
delete stream;
return NULL;
#else // HAVE_ZLIB
// Oops, the subfile is compressed. So actually, return an
// IDecompressStream that wraps around the ISubStream.
IDecompressStream *wrapper = new IDecompressStream(stream, true);
stream = wrapper;
#endif // HAVE_ZLIB
}
if (stream->fail()) {
// Hmm, some inexplicable problem.
delete stream;
return NULL;
}
return stream;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::close_read_subfile
// Access: Published, Static
// Description: Closes a file opened by a previous call to
// open_read_subfile(). This really just deletes the
// istream pointer, but it is recommended to use this
// interface instead of deleting it explicitly, to help
// work around compiler issues.
////////////////////////////////////////////////////////////////////
void Multifile::
close_read_subfile(istream *stream) {
if (stream != (istream *)NULL) {
// For some reason--compiler bug in gcc 3.2?--explicitly deleting
// the stream pointer does not call the appropriate global delete
// function; instead apparently calling the system delete
// function. So we call the delete function by hand instead.
#if !defined(WIN32_VC) && !defined(USE_MEMORY_NOWRAPPERS) && defined(REDEFINE_GLOBAL_OPERATOR_NEW)
stream->~istream();
(*global_operator_delete)(stream);
#else
delete stream;
#endif
}
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::extract_subfile
// Access: Published
// Description: Extracts the nth subfile into a file with the given
// name.
////////////////////////////////////////////////////////////////////
bool Multifile::
extract_subfile(int index, const Filename &filename) {
nassertr(is_read_valid(), false);
nassertr(index >= 0 && index < (int)_subfiles.size(), false);
Filename fname = filename;
fname.set_binary();
fname.make_dir();
pofstream out;
if (!fname.open_write(out, true)) {
express_cat.info()
<< "Unable to write to file " << filename << "\n";
return false;
}
return extract_subfile_to(index, out);
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::extract_subfile_to
// Access: Public
// Description: Extracts the nth subfile to the indicated ostream.
////////////////////////////////////////////////////////////////////
bool Multifile::
extract_subfile_to(int index, ostream &out) {
nassertr(is_read_valid(), false);
nassertr(index >= 0 && index < (int)_subfiles.size(), false);
istream *in = open_read_subfile(index);
if (in == (istream *)NULL) {
return false;
}
static const size_t buffer_size = 1024;
char buffer[buffer_size];
in->read(buffer, buffer_size);
size_t count = in->gcount();
while (count != 0) {
out.write(buffer, count);
in->read(buffer, buffer_size);
count = in->gcount();
}
bool failed = (in->fail() && !in->eof());
close_read_subfile(in);
nassertr(!failed, false);
return (!out.fail());
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::compare_subfile
// Access: Published
// Description: Performs a byte-for-byte comparison of the indicated
// file on disk with the nth subfile. Returns true if
// the files are equivalent, or false if they are
// different (or the file is missing).
////////////////////////////////////////////////////////////////////
bool Multifile::
compare_subfile(int index, const Filename &filename) {
nassertr(is_read_valid(), false);
nassertr(index >= 0 && index < (int)_subfiles.size(), false);
if (!filename.exists()) {
express_cat.info()
<< "File is missing: " << filename << "\n";
return false;
}
istream *in1 = open_read_subfile(index);
if (in1 == (istream *)NULL) {
return false;
}
pifstream in2;
Filename bin_filename = Filename::binary_filename(filename);
if (!bin_filename.open_read(in2)) {
express_cat.info()
<< "Cannot read " << filename << "\n";
return false;
}
// Check the file size.
in2.seekg(0, ios::end);
streampos file_size = in2.tellg();
if (file_size != (streampos)get_subfile_length(index)) {
// The files have different sizes.
close_read_subfile(in1);
return false;
}
// Check the file data, byte-for-byte.
in2.seekg(0);
int byte1 = in1->get();
int byte2 = in2.get();
while (!in1->fail() && !in1->eof() &&
!in2.fail() && !in2.eof()) {
if (byte1 != byte2) {
close_read_subfile(in1);
return false;
}
byte1 = in1->get();
byte2 = in2.get();
}
bool failed = (in1->fail() && !in1->eof()) || (in2.fail() && !in2.eof());
close_read_subfile(in1);
nassertr(!failed, false);
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::output
// Access: Published
// Description:
////////////////////////////////////////////////////////////////////
void Multifile::
output(ostream &out) const {
out << "Multifile " << _multifile_name << ", " << get_num_subfiles()
<< " subfiles.\n";
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::ls
// Access: Published
// Description: Shows a list of all subfiles within the Multifile.
////////////////////////////////////////////////////////////////////
void Multifile::
ls(ostream &out) const {
int num_subfiles = get_num_subfiles();
for (int i = 0; i < num_subfiles; i++) {
string subfile_name = get_subfile_name(i);
out << subfile_name << "\n";
}
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::read_subfile
// Access: Public
// Description: Fills a string with the entire contents of
// the indicated subfile.
////////////////////////////////////////////////////////////////////
bool Multifile::
read_subfile(int index, string &result) {
result = string();
pvector<unsigned char> pv;
if (!read_subfile(index, pv)) {
return false;
}
if (!pv.empty()) {
result.append((const char *)&pv[0], pv.size());
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::read_subfile
// Access: Public
// Description: Fills a pvector with the entire contents of
// the indicated subfile.
////////////////////////////////////////////////////////////////////
bool Multifile::
read_subfile(int index, pvector<unsigned char> &result) {
nassertr(is_read_valid(), false);
nassertr(index >= 0 && index < (int)_subfiles.size(), false);
result.clear();
istream *in = open_read_subfile(index);
if (in == (istream *)NULL) {
return false;
}
static const size_t buffer_size = 1024;
char buffer[buffer_size];
in->read(buffer, buffer_size);
size_t count = in->gcount();
while (count != 0) {
result.insert(result.end(), buffer, buffer + count);
in->read(buffer, buffer_size);
count = in->gcount();
}
bool failed = in->fail() && !in->eof();
close_read_subfile(in);
nassertr(!failed, false);
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::pad_to_streampos
// Access: Private
// Description: Assumes the _write pointer is at the indicated fpos,
// rounds the fpos up to the next legitimate address
// (using normalize_streampos()), and writes enough
// zeroes to the stream to fill the gap. Returns the
// new fpos.
////////////////////////////////////////////////////////////////////
streampos Multifile::
pad_to_streampos(streampos fpos) {
nassertr(_write != (ostream *)NULL, fpos);
nassertr(_write->tellp() == fpos, fpos);
streampos new_fpos = normalize_streampos(fpos);
while (fpos < new_fpos) {
_write->put(0);
fpos += 1; // VC++ doesn't define streampos++ (!)
}
nassertr(_write->tellp() == fpos, fpos);
return fpos;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::add_new_subfile
// Access: Private
// Description: Adds a newly-allocated Subfile pointer to the
// Multifile.
////////////////////////////////////////////////////////////////////
void Multifile::
add_new_subfile(Subfile *subfile, int compression_level) {
if (compression_level != 0) {
#ifndef HAVE_ZLIB
express_cat.warning()
<< "zlib not compiled in; cannot generated compressed multifiles.\n";
compression_level = 0;
#else // HAVE_ZLIB
subfile->_flags |= SF_compressed;
subfile->_compression_level = compression_level;
#endif // HAVE_ZLIB
}
#ifdef HAVE_OPENSSL
if (_encryption_flag) {
subfile->_flags |= SF_encrypted;
}
#endif // HAVE_OPENSSL
if (_next_index != (streampos)0) {
// If we're adding a Subfile to an already-existing Multifile, we
// will eventually need to repack the file.
_needs_repack = true;
}
pair<Subfiles::iterator, bool> insert_result = _subfiles.insert(subfile);
if (!insert_result.second) {
// Hmm, unable to insert. There must already be a subfile by that
// name. Remove the old one.
Subfile *old_subfile = (*insert_result.first);
old_subfile->_flags |= SF_deleted;
// Maybe it was just added to the _new_subfiles list. In this case, remove it from that list.
PendingSubfiles::iterator ni = find(_new_subfiles.begin(), _new_subfiles.end(), old_subfile);
if (ni != _new_subfiles.end()) {
_new_subfiles.erase(ni);
} else {
// Otherwise, add it to the _removed_subfiles list, so we can remove the old one.
_removed_subfiles.push_back(old_subfile);
}
(*insert_result.first) = subfile;
}
_new_subfiles.push_back(subfile);
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::standardize_subfile_name
// Access: Private
// Description: Returns the standard form of the subfile name.
////////////////////////////////////////////////////////////////////
string Multifile::
standardize_subfile_name(const string &subfile_name) const {
Filename name = subfile_name;
name.standardize();
if (name.empty() || name == "/") {
// Invalid empty name.
return string();
}
if (name[0] == '/') {
return name.get_fullpath().substr(1);
} else {
return name.get_fullpath();
}
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::clear_subfiles
// Access: Private
// Description: Removes the set of subfiles from the tables and frees
// their associated memory.
////////////////////////////////////////////////////////////////////
void Multifile::
clear_subfiles() {
PendingSubfiles::iterator pi;
for (pi = _removed_subfiles.begin(); pi != _removed_subfiles.end(); ++pi) {
Subfile *subfile = (*pi);
subfile->rewrite_index_flags(*_write);
delete subfile;
}
_removed_subfiles.clear();
// We don't have to delete the ones in _new_subfiles, because these
// also appear in _subfiles.
_new_subfiles.clear();
Subfiles::iterator fi;
for (fi = _subfiles.begin(); fi != _subfiles.end(); ++fi) {
Subfile *subfile = (*fi);
delete subfile;
}
_subfiles.clear();
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::read_index
// Access: Private
// Description: Reads the Multifile header and index. Returns true
// if successful, false if the Multifile is not valid.
////////////////////////////////////////////////////////////////////
bool Multifile::
read_index() {
nassertr(_read != (IStreamWrapper *)NULL, false);
// We acquire the IStreamWrapper lock for the duration of this
// method.
_read->acquire();
istream *read = _read->get_istream();
char this_header[_header_size];
read->seekg(0);
read->read(this_header, _header_size);
if (read->fail() || read->gcount() != (unsigned)_header_size) {
express_cat.info()
<< "Unable to read Multifile header " << _multifile_name << ".\n";
_read->release();
close();
return false;
}
// Here's a special case: if the multifile begins with a hash
// character, then we skip at least 6 characters (the length of the
// original header string we already read), and continue reading and
// discarding lines of ASCII text, until we come across a nonempty
// line that does not begin with a hash character. This allows a
// P3D application (which is a multifile) to be run directly on the
// command line on Unix-based systems.
_header_prefix = string();
if (this_header[0] == '#') {
_header_prefix = string(this_header, _header_size);
int ch = '#';
while (ch != EOF && ch == '#') {
// Skip to the end of the line.
while (ch != EOF && ch != '\n') {
_header_prefix += ch;
ch = read->get();
}
// Skip to the first non-whitespace character of the line.
while (ch != EOF && (isspace(ch) || ch == '\r')) {
_header_prefix += ch;
ch = read->get();
}
}
// Now fill up the header.
this_header[0] = ch;
read->read(this_header + 1, _header_size - 1);
}
if (memcmp(this_header, _header, _header_size) != 0) {
express_cat.info()
<< _multifile_name << " is not a Multifile.\n";
_read->release();
close();
return false;
}
// Now get the version numbers out.
StreamReader reader(read, false);
_file_major_ver = reader.get_int16();
_file_minor_ver = reader.get_int16();
_scale_factor = reader.get_uint32();
_new_scale_factor = _scale_factor;
if (read->eof() || read->fail()) {
express_cat.info()
<< _multifile_name << " header is truncated.\n";
_read->release();
close();
return false;
}
if (_file_major_ver != _current_major_ver ||
(_file_major_ver == _current_major_ver &&
_file_minor_ver > _current_minor_ver)) {
express_cat.info()
<< _multifile_name << " has version " << _file_major_ver << "."
<< _file_minor_ver << ", expecting version "
<< _current_major_ver << "." << _current_minor_ver << ".\n";
_read->release();
close();
return false;
}
_record_timestamp = true;
if (_file_minor_ver >= 1) {
time_t read_timestamp = reader.get_uint32();
if (read_timestamp == 0) {
// If we read a 0 timestamp from the file, that implies that we
// don't want to record a timestamp in this particular file.
_record_timestamp = false;
} else {
_timestamp = read_timestamp;
}
_timestamp_dirty = false;
}
// Now read the index out.
_next_index = read->tellg();
_next_index = normalize_streampos(_next_index);
read->seekg(_next_index);
_last_index = 0;
streampos index_forward;
Subfile *subfile = new Subfile;
index_forward = subfile->read_index(*read, _next_index, this);
while (index_forward != (streampos)0) {
_last_index = _next_index;
if (subfile->is_deleted()) {
// Ignore deleted Subfiles in the index.
_needs_repack = true;
delete subfile;
} else {
_subfiles.push_back(subfile);
}
if (index_forward != normalize_streampos(read->tellg())) {
// If the index entries don't follow exactly sequentially, the
// file ought to be repacked.
_needs_repack = true;
}
read->seekg(index_forward);
_next_index = index_forward;
subfile = new Subfile;
index_forward = subfile->read_index(*read, _next_index, this);
}
if (subfile->is_index_invalid()) {
express_cat.info()
<< "Error reading index for " << _multifile_name << ".\n";
_read->release();
close();
delete subfile;
return false;
}
// Check if the list is already sorted. If it is not, we need a
// repack.
for (size_t si = 1; si < _subfiles.size() && !_needs_repack; ++si) {
if (*_subfiles[si] < *_subfiles[si - 1]) {
_needs_repack = true;
}
}
if (_needs_repack) {
// At least sort them now.
size_t before_size = _subfiles.size();
_subfiles.sort();
size_t after_size = _subfiles.size();
// If these don't match, the same filename appeared twice in the
// index, which shouldn't be possible.
nassertr(before_size == after_size, true);
}
delete subfile;
_read->release();
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::write_header
// Access: Private
// Description: Writes just the header part of the Multifile, not the
// index.
////////////////////////////////////////////////////////////////////
bool Multifile::
write_header() {
_file_major_ver = _current_major_ver;
_file_minor_ver = _current_minor_ver;
nassertr(_write != (ostream *)NULL, false);
nassertr(_write->tellp() == (streampos)0, false);
_write->write(_header_prefix.data(), _header_prefix.size());
_write->write(_header, _header_size);
StreamWriter writer(_write, false);
writer.add_int16(_current_major_ver);
writer.add_int16(_current_minor_ver);
writer.add_uint32(_scale_factor);
if (_record_timestamp) {
writer.add_uint32(_timestamp);
} else {
writer.add_uint32(0);
_timestamp_dirty = false;
}
_next_index = _write->tellp();
_next_index = pad_to_streampos(_next_index);
_last_index = 0;
if (_write->fail()) {
express_cat.info()
<< "Unable to write header for " << _multifile_name << ".\n";
close();
return false;
}
return true;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Subfile::read_index
// Access: Public
// Description: Reads the index record for the Subfile from the
// indicated istream. Assumes the istream has already
// been positioned to the indicated stream position,
// fpos, the start of the index record. Returns the
// position within the file of the next index record.
////////////////////////////////////////////////////////////////////
streampos Multifile::Subfile::
read_index(istream &read, streampos fpos, Multifile *multifile) {
nassertr(read.tellg() == fpos, fpos);
// First, get the next stream position. We do this separately,
// because if it is zero, we don't get anything else.
StreamReader reader(read);
streampos next_index = multifile->word_to_streampos(reader.get_uint32());
if (read.eof() || read.fail()) {
_flags |= SF_index_invalid;
return 0;
}
if (next_index == (streampos)0) {
return 0;
}
// Now get the rest of the index.
_index_start = fpos;
_data_start = multifile->word_to_streampos(reader.get_uint32());
_data_length = reader.get_uint32();
_flags = reader.get_uint16();
if ((_flags & (SF_compressed | SF_encrypted)) != 0) {
_uncompressed_length = reader.get_uint32();
} else {
_uncompressed_length = _data_length;
}
if (multifile->_file_minor_ver < 1) {
_timestamp = multifile->get_timestamp();
} else {
_timestamp = reader.get_uint32();
if (_timestamp == 0) {
_timestamp = multifile->get_timestamp();
}
}
size_t name_length = reader.get_uint16();
if (read.eof() || read.fail()) {
_flags |= SF_index_invalid;
return 0;
}
// And finally, get the rest of the name.
char *name_buffer = (char *)PANDA_MALLOC_ARRAY(name_length);
nassertr(name_buffer != (char *)NULL, next_index);
for (size_t ni = 0; ni < name_length; ni++) {
name_buffer[ni] = read.get() ^ 0xff;
}
_name = string(name_buffer, name_length);
PANDA_FREE_ARRAY(name_buffer);
if (read.eof() || read.fail()) {
_flags |= SF_index_invalid;
return 0;
}
return next_index;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Subfile::write_index
// Access: Public
// Description: Writes the index record for the Subfile to the
// indicated ostream. Assumes the istream has already
// been positioned to the indicated stream position,
// fpos, the start of the index record, and that this is
// the effective end of the file. Returns the position
// within the file of the next index record.
//
// The _index_start member is updated by this operation.
////////////////////////////////////////////////////////////////////
streampos Multifile::Subfile::
write_index(ostream &write, streampos fpos, Multifile *multifile) {
nassertr(write.tellp() == fpos, fpos);
_index_start = fpos;
// This will be the contents of this particular index record. We
// build it up first since it will be variable length.
Datagram dg;
dg.add_uint32(multifile->streampos_to_word(_data_start));
dg.add_uint32(_data_length);
dg.add_uint16(_flags);
if ((_flags & (SF_compressed | SF_encrypted)) != 0) {
dg.add_uint32(_uncompressed_length);
}
dg.add_uint32(_timestamp);
dg.add_uint16(_name.length());
// For no real good reason, we'll invert all the bits in the name.
// The only reason we do this is to make it inconvenient for a
// casual browser of the Multifile to discover the names of the
// files stored within it. Naturally, this isn't real obfuscation
// or security.
string::iterator ni;
for (ni = _name.begin(); ni != _name.end(); ++ni) {
dg.add_int8((*ni) ^ 0xff);
}
size_t this_index_size = 4 + dg.get_length();
// Plus, we will write out the next index address first.
streampos next_index = fpos + (streampos)this_index_size;
Datagram idg;
idg.add_uint32(multifile->streampos_to_word(next_index));
write.write((const char *)idg.get_data(), idg.get_length());
write.write((const char *)dg.get_data(), dg.get_length());
return next_index;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Subfile::write_data
// Access: Public
// Description: Writes the data record for the Subfile to the
// indicated ostream: the actual contents of the
// Subfile. Assumes the istream has already been
// positioned to the indicated stream position, fpos,
// the start of the data record, and that this is the
// effective end of the file. Returns the position
// within the file of the next data record.
//
// The _data_start, _data_length, and
// _uncompressed_length members are updated by this
// operation.
//
// If the "read" pointer is non-NULL, it is the readable
// istream of a Multifile in which the Subfile might
// already be packed. This is used for reading the
// contents of the Subfile during a repack() operation.
////////////////////////////////////////////////////////////////////
streampos Multifile::Subfile::
write_data(ostream &write, istream *read, streampos fpos,
Multifile *multifile) {
nassertr(write.tellp() == fpos, fpos);
istream *source = _source;
pifstream source_file;
if (source == (istream *)NULL && !_source_filename.empty()) {
// If we have a filename, open it up and read that.
if (!_source_filename.open_read(source_file)) {
// Unable to open the source file.
express_cat.info()
<< "Unable to read " << _source_filename << ".\n";
_flags |= SF_data_invalid;
_data_length = 0;
_uncompressed_length = 0;
} else {
source = &source_file;
}
}
if (source == (istream *)NULL) {
// We don't have any source data. Perhaps we're reading from an
// already-packed Subfile (e.g. during repack()).
if (read == (istream *)NULL) {
// No, we're just screwed.
express_cat.info()
<< "No source for subfile " << _name << ".\n";
_flags |= SF_data_invalid;
} else {
// Read the data from the original Multifile.
read->seekg(_data_start);
for (size_t p = 0; p < _data_length; p++) {
int byte = read->get();
if (read->eof() || read->fail()) {
// Unexpected EOF or other failure on the source file.
express_cat.info()
<< "Unexpected EOF for subfile " << _name << ".\n";
_flags |= SF_data_invalid;
break;
}
write.put(byte);
}
}
} else {
// We do have source data. Copy it in, and also measure its
// length.
ostream *putter = &write;
bool delete_putter = false;
#ifndef HAVE_OPENSSL
// Without OpenSSL, we can't support encryption. The flag had
// better not be set.
nassertr((_flags & SF_encrypted) == 0, fpos);
#else // HAVE_ZLIB
if ((_flags & SF_encrypted) != 0) {
// Write it encrypted.
OEncryptStream *encrypt = new OEncryptStream;
encrypt->set_iteration_count(multifile->_encryption_iteration_count);
encrypt->open(putter, delete_putter, multifile->_encryption_password);
putter = encrypt;
delete_putter = true;
// Also write the encrypt_header to the beginning of the
// encrypted stream, so we can validate the password on
// decryption.
putter->write(_encrypt_header, _encrypt_header_size);
}
#endif // HAVE_ZLIB
#ifndef HAVE_ZLIB
// Without ZLIB, we can't support compression. The flag had
// better not be set.
nassertr((_flags & SF_compressed) == 0, fpos);
#else // HAVE_ZLIB
if ((_flags & SF_compressed) != 0) {
// Write it compressed.
putter = new OCompressStream(putter, delete_putter, _compression_level);
delete_putter = true;
}
#endif // HAVE_ZLIB
streampos write_start = fpos;
_uncompressed_length = 0;
static const size_t buffer_size = 1024;
char buffer[buffer_size];
source->read(buffer, buffer_size);
size_t count = source->gcount();
while (count != 0) {
_uncompressed_length += count;
putter->write(buffer, count);
source->read(buffer, buffer_size);
count = source->gcount();
}
if (delete_putter) {
delete putter;
}
streampos write_end = write.tellp();
_data_length = (size_t)(write_end - write_start);
}
// We can't set _data_start until down here, after we have read the
// Subfile. (In case we are running during repack()).
_data_start = fpos;
// Get the modification timestamp for this subfile. This is read
// from the source file, if we have a filename; otherwise, it's the
// current time.
if (!_source_filename.empty()) {
_timestamp = _source_filename.get_timestamp();
}
if (_timestamp == 0) {
_timestamp = time(NULL);
}
_source = (istream *)NULL;
_source_filename = Filename();
source_file.close();
return fpos + (streampos)_data_length;
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Subfile::rewrite_index_data_start
// Access: Public
// Description: Seeks within the indicate pfstream back to the index
// record and rewrites just the _data_start and
// _data_length part of the index record.
////////////////////////////////////////////////////////////////////
void Multifile::Subfile::
rewrite_index_data_start(ostream &write, Multifile *multifile) {
nassertv(_index_start != (streampos)0);
static const size_t data_start_offset = 4;
size_t data_start_pos = _index_start + (streampos)data_start_offset;
write.seekp(data_start_pos);
nassertv(!write.fail());
StreamWriter writer(write);
writer.add_uint32(multifile->streampos_to_word(_data_start));
writer.add_uint32(_data_length);
writer.add_uint16(_flags);
if ((_flags & (SF_compressed | SF_encrypted)) != 0) {
writer.add_uint32(_uncompressed_length);
}
if (multifile->_record_timestamp) {
writer.add_uint32(_timestamp);
} else {
writer.add_uint32(0);
}
}
////////////////////////////////////////////////////////////////////
// Function: Multifile::Subfile::rewrite_index_flags
// Access: Public
// Description: Seeks within the indicate pfstream back to the index
// record and rewrites just the _flags part of the
// index record.
////////////////////////////////////////////////////////////////////
void Multifile::Subfile::
rewrite_index_flags(ostream &write) {
// If the subfile has never even been recorded to disk, we don't
// need to do anything at all in this function.
if (_index_start != (streampos)0) {
static const size_t flags_offset = 4 + 4 + 4;
size_t flags_pos = _index_start + (streampos)flags_offset;
write.seekp(flags_pos);
nassertv(!write.fail());
StreamWriter writer(write);
writer.add_uint16(_flags);
}
}