/** * 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." * * @file fileSpec.cxx * @author drose * @date 2009-06-29 */ #include "fileSpec.h" #include "wstring_encode.h" #include #include #include #include #include #include #include #ifdef _WIN32 #include #include #define utimbuf _utimbuf #else #include #endif using std::istream; using std::ostream; using std::string; using std::wstring; /** * */ FileSpec:: FileSpec() { _size = 0; _timestamp = 0; memset(_hash, 0, hash_size); _got_hash = false; _actual_file = nullptr; } /** * */ FileSpec:: FileSpec(const FileSpec ©) : _filename(copy._filename), _size(copy._size), _timestamp(copy._timestamp), _got_hash(copy._got_hash) { memcpy(_hash, copy._hash, hash_size); _actual_file = nullptr; } /** * */ void FileSpec:: operator = (const FileSpec ©) { _filename = copy._filename; _size = copy._size; _timestamp = copy._timestamp; memcpy(_hash, copy._hash, hash_size); _got_hash = copy._got_hash; } /** * */ FileSpec:: ~FileSpec() { if (_actual_file != nullptr) { delete _actual_file; } } /** * Reads the data from the indicated XML file. */ void FileSpec:: load_xml(TiXmlElement *xelement) { const char *filename = xelement->Attribute("filename"); if (filename != nullptr) { _filename = filename; } const char *size = xelement->Attribute("size"); if (size != nullptr) { char *endptr; _size = strtoul(size, &endptr, 10); } const char *timestamp = xelement->Attribute("timestamp"); if (timestamp != nullptr) { char *endptr; _timestamp = strtoul(timestamp, &endptr, 10); } _got_hash = false; const char *hash = xelement->Attribute("hash"); if (hash != nullptr && strlen(hash) == (hash_size * 2)) { // Decode the hex hash string. _got_hash = decode_hex(_hash, hash, hash_size); } } /** * Stores the data to the indicated XML file. */ void FileSpec:: store_xml(TiXmlElement *xelement) { if (!_filename.empty()) { xelement->SetAttribute("filename", _filename); } if (_size != 0) { xelement->SetAttribute("size", _size); } if (_timestamp != 0) { xelement->SetAttribute("timestamp", (int)_timestamp); } if (_got_hash) { char hash[hash_size * 2 + 1]; encode_hex(hash, _hash, hash_size); hash[hash_size * 2] = '\0'; xelement->SetAttribute("hash", hash); } } /** * Performs a quick test to ensure the file has not been modified. This test * is vulnerable to people maliciously attempting to fool the program (by * setting datestamps etc.). * * Returns true if it is intact, false if it needs to be redownloaded. */ bool FileSpec:: quick_verify(const string &package_dir) { return quick_verify_pathname(get_pathname(package_dir)); } /** * Works like quick_verify(), above, with an explicit pathname. Useful for * verifying the copy of a file in a temporary location. */ bool FileSpec:: quick_verify_pathname(const string &pathname) { if (_actual_file != nullptr) { delete _actual_file; _actual_file = nullptr; } int result = 1; #ifdef _WIN32 struct _stat st; wstring pathname_w; if (string_to_wstring(pathname_w, pathname)) { result = _wstat(pathname_w.c_str(), &st); } #else // _WIN32 struct stat st; result = stat(pathname.c_str(), &st); #endif // _WIN32 if (result != 0) { // cerr << "file not found: " << _filename << "\n"; return false; } if (st.st_size != _size) { // If the size is wrong, the file fails. cerr << "size wrong: " << // _filename << "\n"; return false; } if (st.st_mtime == _timestamp) { // If the size is right and the timestamp is right, the file passes. cerr // << "file ok: " << _filename << "\n"; return true; } // cerr << "modification time wrong: " << _filename << "\n"; // If the size is right but the timestamp is wrong, the file soft-fails. We // follow this up with a hash check. if (!priv_check_hash(pathname, &st)) { // Hard fail, the hash is wrong. cerr << "hash check wrong: " << // _filename << "\n"; return false; } // cerr << "hash check ok: " << _filename << "\n"; // The hash is OK after all. Change the file's timestamp back to what we // expect it to be, so we can quick-verify it successfully next time. utimbuf utb; utb.actime = st.st_atime; utb.modtime = _timestamp; #ifdef _WIN32 _wutime(pathname_w.c_str(), &utb); #else // _WIN32 utime(pathname.c_str(), &utb); #endif // _WIN32 return true; } /** * Performs a more thorough test to ensure the file has not been modified. * This test is less vulnerable to malicious attacks, since it reads and * verifies the entire file. * * Returns true if it is intact, false if it needs to be redownloaded. */ bool FileSpec:: full_verify(const string &package_dir) { if (_actual_file != nullptr) { delete _actual_file; _actual_file = nullptr; } string pathname = get_pathname(package_dir); int result = 1; #ifdef _WIN32 struct _stat st; wstring pathname_w; if (string_to_wstring(pathname_w, pathname)) { result = _wstat(pathname_w.c_str(), &st); } #else // _WIN32 struct stat st; result = stat(pathname.c_str(), &st); #endif // _WIN32 if (result != 0) { // cerr << "file not found: " << _filename << "\n"; return false; } if (st.st_size != _size) { // If the size is wrong, the file fails. cerr << "size wrong: " << // _filename << "\n"; return false; } if (!priv_check_hash(pathname, &st)) { // Hard fail, the hash is wrong. cerr << "hash check wrong: " << // _filename << "\n"; return false; } // cerr << "hash check ok: " << _filename << "\n"; // The hash is OK. If the timestamp is wrong, change it back to what we // expect it to be, so we can quick-verify it successfully next time. if (st.st_mtime != _timestamp) { utimbuf utb; utb.actime = st.st_atime; utb.modtime = _timestamp; #ifdef _WIN32 _wutime(pathname_w.c_str(), &utb); #else // _WIN32 utime(pathname.c_str(), &utb); #endif // _WIN32 } return true; } /** * Returns a FileSpec that represents the actual data read on disk. This will * read the disk to determine the data if necessary. */ const FileSpec *FileSpec:: force_get_actual_file(const string &pathname) { if (_actual_file == nullptr) { #ifdef _WIN32 struct _stat st; wstring pathname_w; if (string_to_wstring(pathname_w, pathname)) { _wstat(pathname_w.c_str(), &st); } #else // _WIN32 struct stat st; stat(pathname.c_str(), &st); #endif // _WIN32 priv_check_hash(pathname, &st); } return _actual_file; } /** * Returns true if the file has the expected md5 hash, false otherwise. */ bool FileSpec:: check_hash(const string &pathname) const { FileSpec other; if (!other.read_hash(pathname)) { return false; } return (memcmp(_hash, other._hash, hash_size) == 0); } /** * Computes the hash from the indicated pathname and stores it within the * FileSpec. */ bool FileSpec:: read_hash(const string &pathname) { memset(_hash, 0, hash_size); _got_hash = false; ifstream stream; #ifdef _WIN32 wstring pathname_w; if (string_to_wstring(pathname_w, pathname)) { stream.open(pathname_w.c_str(), std::ios::in | std::ios::binary); } #else // _WIN32 stream.open(pathname.c_str(), std::ios::in | std::ios::binary); #endif // _WIN32 if (!stream) { // cerr << "unable to read " << pathname << "\n"; return false; } MD5_CTX ctx; MD5_Init(&ctx); static const int buffer_size = 4096; char buffer[buffer_size]; stream.read(buffer, buffer_size); size_t count = stream.gcount(); while (count != 0) { MD5_Update(&ctx, buffer, count); stream.read(buffer, buffer_size); count = stream.gcount(); } MD5_Final(_hash, &ctx); _got_hash = true; return true; } /** * Reads the hash from the next 16 bytes on the indicated istream, in the same * unusual order observed by Panda's HashVal::read_stream() method. */ bool FileSpec:: read_hash_stream(istream &in) { for (int i = 0; i < hash_size; i += 4) { unsigned int a = in.get(); unsigned int b = in.get(); unsigned int c = in.get(); unsigned int d = in.get(); _hash[i + 0] = d; _hash[i + 1] = c; _hash[i + 2] = b; _hash[i + 3] = a; } return !in.fail(); } /** * Returns < 0 if this hash sorts before the other hash, > 0 if it sorts * after, 0 if they are the same. */ int FileSpec:: compare_hash(const FileSpec &other) const { return memcmp(_hash, other._hash, hash_size); } /** * Describes the data in the FileSpec. */ void FileSpec:: write(ostream &out) const { out << "filename: " << _filename << ", " << _size << " bytes, " << asctime(localtime(&_timestamp)); // asctime includes a newline. out << "hash: "; stream_hex(out, _hash, hash_size); out << "\n"; } /** * Writes just the hash code. */ void FileSpec:: output_hash(ostream &out) const { stream_hex(out, _hash, hash_size); } /** * Returns true if the file has the expected md5 hash, false otherwise. * Updates _actual_file with the data read from disk, including the hash, for * future reference. * * The parameter stp is a pointer to a stat structure. It's declared as a * void * to get around issues with the nonstandard declaration of this * structure in Windows. */ bool FileSpec:: priv_check_hash(const string &pathname, void *stp) { const struct stat &st = *(const struct stat *)stp; assert(_actual_file == nullptr); _actual_file = new FileSpec; _actual_file->_filename = pathname; _actual_file->_size = st.st_size; _actual_file->_timestamp = st.st_mtime; if (!_actual_file->read_hash(pathname)) { return false; } return (memcmp(_hash, _actual_file->_hash, hash_size) == 0); } /** * Decodes the hex string in source into the character array in dest. dest * must have has least size bytes; source must have size * 2 bytes. * * Returns true on success, false if there was a non-hex digit in the string. */ bool FileSpec:: decode_hex(unsigned char *dest, const char *source, size_t size) { for (size_t i = 0; i < size; ++i) { int high = decode_hexdigit(source[i * 2]); int low = decode_hexdigit(source[i * 2 + 1]); if (high < 0 || low < 0) { return false; } dest[i] = (high << 4) | low; } return true; } /** * Encodes a character array into a hex string for output. dest must have at * least size * 2 bytes; source must have size bytes. The result is not null- * terminated. */ void FileSpec:: encode_hex(char *dest, const unsigned char *source, size_t size) { for (size_t i = 0; i < size; ++i) { int high = (source[i] >> 4) & 0xf; int low = source[i] & 0xf; dest[2 * i] = encode_hexdigit(high); dest[2 * i + 1] = encode_hexdigit(low); } } /** * Writes the indicated buffer as a string of hex characters to the given * ostream. */ void FileSpec:: stream_hex(ostream &out, const unsigned char *source, size_t size) { for (size_t i = 0; i < size; ++i) { int high = (source[i] >> 4) & 0xf; int low = source[i] & 0xf; out.put(encode_hexdigit(high)); out.put(encode_hexdigit(low)); } }