blob: e1f17b1db0167e47991268c148830473ad9f8211 [file] [log] [blame]
#include "talk/base/basicdefs.h"
#include "talk/base/basictypes.h"
#include "talk/base/tarstream.h"
#include "talk/base/pathutils.h"
#include "talk/base/stringutils.h"
#include "talk/base/common.h"
using namespace talk_base;
///////////////////////////////////////////////////////////////////////////////
// TarStream
///////////////////////////////////////////////////////////////////////////////
TarStream::TarStream() : mode_(M_NONE), next_block_(NB_NONE), block_pos_(0),
current_(NULL), current_bytes_(0) {
}
TarStream::~TarStream() {
Close();
}
bool TarStream::AddFilter(const std::string& pathname) {
if (pathname.empty())
return false;
Pathname archive_path(pathname);
archive_path.SetFolderDelimiter('/');
archive_path.Normalize();
filters_.push_back(archive_path.pathname());
return true;
}
bool TarStream::Open(const std::string& folder, bool read) {
Close();
Pathname root_folder;
root_folder.SetFolder(folder);
root_folder.Normalize();
root_folder_.assign(root_folder.folder());
if (read) {
std::string pattern(root_folder_);
DirectoryIterator *iter = new DirectoryIterator();
if (iter->Iterate(pattern) == false) {
delete iter;
return false;
}
mode_ = M_READ;
find_.push_front(iter);
next_block_ = NB_FILE_HEADER;
block_pos_ = BLOCK_SIZE;
int error;
if (SR_SUCCESS != ProcessNextEntry(find_.front(), &error)) {
return false;
}
} else {
if (!Filesystem::CreateFolder(root_folder_)) {
return false;
}
mode_ = M_WRITE;
next_block_ = NB_FILE_HEADER;
block_pos_ = 0;
}
return true;
}
StreamState TarStream::GetState() const {
return (M_NONE == mode_) ? SS_CLOSED : SS_OPEN;
}
StreamResult TarStream::Read(void* buffer, size_t buffer_len,
size_t* read, int* error) {
if (M_READ != mode_) {
return SR_EOS;
}
return ProcessBuffer(buffer, buffer_len, read, error);
}
StreamResult TarStream::Write(const void* data, size_t data_len,
size_t* written, int* error) {
if (M_WRITE != mode_) {
return SR_EOS;
}
// Note: data is not modified unless M_READ == mode_
return ProcessBuffer(const_cast<void*>(data), data_len, written, error);
}
void TarStream::Close() {
root_folder_.clear();
next_block_ = NB_NONE;
block_pos_ = 0;
delete current_;
current_ = NULL;
current_bytes_ = 0;
for (DirectoryList::iterator it = find_.begin(); it != find_.end(); ++it) {
delete(*it);
}
find_.clear();
subfolder_.clear();
}
StreamResult TarStream::ProcessBuffer(void* buffer, size_t buffer_len,
size_t* consumed, int* error) {
size_t local_consumed;
if (!consumed) consumed = &local_consumed;
int local_error;
if (!error) error = &local_error;
StreamResult result = SR_SUCCESS;
*consumed = 0;
while (*consumed < buffer_len) {
size_t available = BLOCK_SIZE - block_pos_;
if (available == 0) {
result = ProcessNextBlock(error);
if (SR_SUCCESS != result) {
break;
}
} else {
size_t bytes_to_copy = talk_base::_min(available, buffer_len - *consumed);
char* buffer_ptr = static_cast<char*>(buffer) + *consumed;
char* block_ptr = block_ + block_pos_;
if (M_READ == mode_) {
memcpy(buffer_ptr, block_ptr, bytes_to_copy);
} else {
memcpy(block_ptr, buffer_ptr, bytes_to_copy);
}
*consumed += bytes_to_copy;
block_pos_ += bytes_to_copy;
}
}
// SR_EOS means no data was consumed on this operation. So we may need to
// return SR_SUCCESS instead, and then we will return SR_EOS next time.
if ((SR_EOS == result) && (*consumed > 0)) {
result = SR_SUCCESS;
}
return result;
}
StreamResult TarStream::ProcessNextBlock(int* error) {
ASSERT(NULL != error);
ASSERT(M_NONE != mode_);
ASSERT(BLOCK_SIZE == block_pos_);
StreamResult result;
if (NB_NONE == next_block_) {
return SR_EOS;
} else if (NB_TRAILER == next_block_) {
// Trailer block is zeroed
result = ProcessEmptyBlock(0, error);
if (SR_SUCCESS != result)
return result;
next_block_ = NB_NONE;
} else if (NB_FILE_HEADER == next_block_) {
if (M_READ == mode_) {
result = ReadNextFile(error);
} else {
result = WriteNextFile(error);
}
// If there are no more files, we are at the first trailer block
if (SR_EOS == result) {
block_pos_ = 0;
next_block_ = NB_TRAILER;
result = ProcessEmptyBlock(0, error);
}
if (SR_SUCCESS != result)
return result;
} else if (NB_DATA == next_block_) {
size_t block_consumed = 0;
size_t block_available = talk_base::_min<size_t>(BLOCK_SIZE, current_bytes_);
while (block_consumed < block_available) {
void* block_ptr = static_cast<char*>(block_) + block_consumed;
size_t available = block_available - block_consumed, consumed;
if (M_READ == mode_) {
ASSERT(NULL != current_);
result = current_->Read(block_ptr, available, &consumed, error);
} else if (current_) {
result = current_->Write(block_ptr, available, &consumed, error);
} else {
consumed = available;
result = SR_SUCCESS;
}
switch (result) {
case SR_ERROR:
return result;
case SR_BLOCK:
case SR_EOS:
ASSERT(false);
*error = 0; // TODO: make errors
return SR_ERROR;
case SR_SUCCESS:
block_consumed += consumed;
break;
}
}
current_bytes_ -= block_consumed;
if (current_bytes_ == 0) {
// The remainder of the block is zeroed
result = ProcessEmptyBlock(block_consumed, error);
if (SR_SUCCESS != result)
return result;
delete current_;
current_ = NULL;
next_block_ = NB_FILE_HEADER;
}
} else {
ASSERT(false);
}
block_pos_ = 0;
return SR_SUCCESS;
}
StreamResult TarStream::ProcessEmptyBlock(size_t start, int* error) {
ASSERT(NULL != error);
ASSERT(M_NONE != mode_);
if (M_READ == mode_) {
memset(block_ + start, 0, BLOCK_SIZE - start);
} else {
if (!talk_base::memory_check(block_ + start, 0, BLOCK_SIZE - start)) {
*error = 0; // TODO: make errors
return SR_ERROR;
}
}
return SR_SUCCESS;
}
StreamResult TarStream::ReadNextFile(int* error) {
ASSERT(NULL != error);
ASSERT(M_READ == mode_);
ASSERT(NB_FILE_HEADER == next_block_);
ASSERT(BLOCK_SIZE == block_pos_);
ASSERT(NULL == current_);
// ReadNextFile conducts a depth-first recursive search through the directory
// tree. find_ maintains a stack of open directory handles, which
// corresponds to our current position in the tree. At any point, the
// directory at the top (front) of the stack is being enumerated. If a
// directory is found, it is opened and pushed onto the top of the stack.
// When a directory enumeration completes, that directory is popped off the
// top of the stack.
// Note: Since ReadNextFile can only return one block of data at a time, we
// cannot simultaneously return the entry for a directory, and the entry for
// the first element in that directory at the same time. In this case, we
// push a NULL entry onto the find_ stack, which indicates that the next
// iteration should begin enumeration of the "new" directory.
StreamResult result = SR_SUCCESS;
while (BLOCK_SIZE == block_pos_) {
ASSERT(!find_.empty());
if (NULL != find_.front()) {
if (find_.front()->Next()) {
result = ProcessNextEntry(find_.front(), error);
if (SR_SUCCESS != result) {
return result;
}
continue;
}
delete(find_.front());
} else {
Pathname pattern(root_folder_);
pattern.AppendFolder(subfolder_);
find_.front() = new DirectoryIterator();
if (find_.front()->Iterate(pattern.pathname())) {
result = ProcessNextEntry(find_.front(), error);
if (SR_SUCCESS != result) {
return result;
}
continue;
}
// TODO: should this be an error?
LOG_F(LS_WARNING) << "Couldn't open folder: " << pattern.pathname();
}
find_.pop_front();
subfolder_ = Pathname(subfolder_).parent_folder();
if (find_.empty()) {
return SR_EOS;
}
}
ASSERT(0 == block_pos_);
return SR_SUCCESS;
}
StreamResult TarStream::WriteNextFile(int* error) {
ASSERT(NULL != error);
ASSERT(M_WRITE == mode_);
ASSERT(NB_FILE_HEADER == next_block_);
ASSERT(BLOCK_SIZE == block_pos_);
ASSERT(NULL == current_);
ASSERT(0 == current_bytes_);
std::string pathname, link, linked_name, magic, mversion;
size_t file_size, modify_time, unused, checksum;
size_t block_data = 0;
ReadFieldS(block_data, 100, &pathname);
ReadFieldN(block_data, 8, &unused); // mode
ReadFieldN(block_data, 8, &unused); // owner uid
ReadFieldN(block_data, 8, &unused); // owner gid
ReadFieldN(block_data, 12, &file_size);
ReadFieldN(block_data, 12, &modify_time);
ReadFieldN(block_data, 8, &checksum);
if (checksum == 0)
block_data -= 8; // back-compatiblity
ReadFieldS(block_data, 1, &link);
ReadFieldS(block_data, 100, &linked_name); // name of linked file
ReadFieldS(block_data, 6, &magic);
ReadFieldS(block_data, 2, &mversion);
if (pathname.empty())
return SR_EOS;
std::string user, group, dev_major, dev_minor, prefix;
if (magic == "ustar" || magic == "ustar ") {
ReadFieldS(block_data, 32, &user);
ReadFieldS(block_data, 32, &group);
ReadFieldS(block_data, 8, &dev_major);
ReadFieldS(block_data, 8, &dev_minor);
ReadFieldS(block_data, 155, &prefix);
pathname = prefix + pathname;
}
// Rest of the block must be empty
StreamResult result = ProcessEmptyBlock(block_data, error);
if (SR_SUCCESS != result) {
return result;
}
Pathname archive_path(pathname);
archive_path.SetFolderDelimiter('/');
archive_path.Normalize();
bool is_folder = archive_path.filename().empty();
if (is_folder) {
ASSERT(NB_FILE_HEADER == next_block_);
ASSERT(0 == file_size);
} else if (file_size > 0) {
// We assign current_bytes_ because we must skip over the upcoming data
// segments, regardless of whether we want to write them.
next_block_ = NB_DATA;
current_bytes_ = file_size;
}
if (!CheckFilter(archive_path.pathname())) {
// If it's a directory, we will ignore it and all children by nature of
// filter prefix matching. If it is a file, we will ignore it because
// current_ is NULL.
return SR_SUCCESS;
}
// Sanity checks:
// 1) No .. path segments
if (archive_path.pathname().find("../") != std::string::npos) {
LOG_F(LS_WARNING) << "Skipping path with .. entry: "
<< archive_path.pathname();
return SR_SUCCESS;
}
// 2) No drive letters
if (archive_path.pathname().find(':') != std::string::npos) {
LOG_F(LS_WARNING) << "Skipping path with drive letter: "
<< archive_path.pathname();
return SR_SUCCESS;
}
// 3) No absolute paths
if (archive_path.pathname().find("//") != std::string::npos) {
LOG_F(LS_WARNING) << "Skipping absolute path: "
<< archive_path.pathname();
return SR_SUCCESS;
}
Pathname local_path(root_folder_);
local_path.AppendPathname(archive_path.pathname());
local_path.Normalize();
if (is_folder) {
if (!Filesystem::CreateFolder(local_path)) {
LOG_F(LS_WARNING) << "Couldn't create folder: " << local_path.pathname();
*error = 0; // TODO
return SR_ERROR;
}
} else {
FileStream* stream = new FileStream;
if (!stream->Open(local_path.pathname().c_str(), "wb")) {
LOG_F(LS_WARNING) << "Couldn't create file: " << local_path.pathname();
*error = 0; // TODO
delete stream;
return SR_ERROR;
}
if (file_size > 0) {
current_ = stream;
} else {
stream->Close();
delete stream;
}
}
SignalNextEntry(archive_path.filename(), current_bytes_);
return SR_SUCCESS;
}
StreamResult TarStream::ProcessNextEntry(const DirectoryIterator *data, int *error) {
ASSERT(M_READ == mode_);
ASSERT(NB_FILE_HEADER == next_block_);
ASSERT(BLOCK_SIZE == block_pos_);
ASSERT(NULL == current_);
ASSERT(0 == current_bytes_);
if (data->IsDirectory() &&
(data->Name() == "." || data->Name() == ".."))
return SR_SUCCESS;
Pathname archive_path;
archive_path.SetFolder(subfolder_);
if (data->IsDirectory()) {
archive_path.AppendFolder(data->Name());
} else {
archive_path.SetFilename(data->Name());
}
archive_path.SetFolderDelimiter('/');
archive_path.Normalize();
if (!CheckFilter(archive_path.pathname()))
return SR_SUCCESS;
if (archive_path.pathname().length() > 255) {
// Cannot send a file name longer than 255 (yet)
return SR_ERROR;
}
Pathname local_path(root_folder_);
local_path.AppendPathname(archive_path.pathname());
local_path.Normalize();
if (data->IsDirectory()) {
// Note: the NULL handle indicates that we need to open the folder next
// time.
find_.push_front(NULL);
subfolder_ = archive_path.pathname();
} else {
FileStream* stream = new FileStream;
if (!stream->Open(local_path.pathname().c_str(), "rb")) {
// TODO: Should this be an error?
LOG_F(LS_WARNING) << "Couldn't open file: " << local_path.pathname();
delete stream;
return SR_SUCCESS;
}
current_ = stream;
current_bytes_ = data->FileSize();
}
time_t modify_time = data->FileModifyTime();
std::string pathname = archive_path.pathname();
std::string magic, user, group, dev_major, dev_minor, prefix;
std::string name = pathname;
bool ustar = false;
if (name.length() > 100) {
ustar = true;
// Put last 100 characters into the name, and rest in prefix
size_t path_length = pathname.length();
prefix = pathname.substr(0, path_length - 100);
name = pathname.substr(path_length - 100);
}
size_t block_data = 0;
memset(block_, 0, BLOCK_SIZE);
WriteFieldS(block_data, 100, name.c_str());
WriteFieldS(block_data, 8, data->IsDirectory() ? "777" : "666"); // mode
WriteFieldS(block_data, 8, "5"); // owner uid
WriteFieldS(block_data, 8, "5"); // owner gid
WriteFieldN(block_data, 12, current_bytes_);
WriteFieldN(block_data, 12, modify_time);
WriteFieldS(block_data, 8, " "); // Checksum. To be filled in later.
WriteFieldS(block_data, 1, data->IsDirectory() ? "5" : "0"); // link indicator (0 == normal file, 5 == directory)
WriteFieldS(block_data, 100, ""); // name of linked file
if (ustar) {
WriteFieldS(block_data, 6, "ustar");
WriteFieldS(block_data, 2, "");
WriteFieldS(block_data, 32, user.c_str());
WriteFieldS(block_data, 32, group.c_str());
WriteFieldS(block_data, 8, dev_major.c_str());
WriteFieldS(block_data, 8, dev_minor.c_str());
WriteFieldS(block_data, 155, prefix.c_str());
}
// Rest of the block must be empty
StreamResult result = ProcessEmptyBlock(block_data, error);
WriteChecksum();
block_pos_ = 0;
if (current_bytes_ > 0) {
next_block_ = data->IsDirectory() ? NB_FILE_HEADER : NB_DATA;
}
SignalNextEntry(archive_path.filename(), current_bytes_);
return result;
}
void TarStream::WriteChecksum() {
unsigned int sum = 0;
for (int i = 0; i < BLOCK_SIZE; i++)
sum += static_cast<unsigned char>(block_[i]);
sprintf(block_ + 148, "%06o", sum);
}
bool TarStream::CheckFilter(const std::string& pathname) {
if (filters_.empty())
return true;
// pathname is allowed when there is a filter which:
// A) Equals name
// B) Matches a folder prefix of name
for (size_t i=0; i<filters_.size(); ++i) {
const std::string& filter = filters_[i];
// Make sure the filter is a prefix of name
if (_strnicmp(pathname.c_str(), filter.data(), filter.length()) != 0)
continue;
// If the filter is not a directory, must match exactly
if (!Pathname::IsFolderDelimiter(filter[filter.length()-1])
&& (filter.length() != pathname.length()))
continue;
return true;
}
return false;
}
void TarStream::WriteFieldN(size_t& pos, size_t max_len, size_t numeric_field) {
WriteFieldF(pos, max_len, "%.*o", max_len - 1, numeric_field);
}
void TarStream::WriteFieldS(size_t& pos, size_t max_len,
const char* string_field) {
ASSERT(pos + max_len <= BLOCK_SIZE);
size_t len = strlen(string_field);
size_t use_len = _min(len, max_len);
memcpy(block_ + pos, string_field, use_len);
pos += max_len;
}
void TarStream::WriteFieldF(size_t& pos, size_t max_len,
const char* format, ...) {
va_list args;
va_start(args, format);
char buffer[BLOCK_SIZE];
vsprintfn(buffer, ARRAY_SIZE(buffer), format, args);
WriteFieldS(pos, max_len, buffer);
va_end(args);
}
void TarStream::ReadFieldN(size_t& pos, size_t max_len, size_t* numeric_field) {
ASSERT(NULL != numeric_field);
std::string buffer;
ReadFieldS(pos, max_len, &buffer);
int value;
if (!buffer.empty() && (1 == sscanf(buffer.c_str(), "%o", &value))) {
*numeric_field = value;
} else {
*numeric_field = 0;
}
}
void TarStream::ReadFieldS(size_t& pos, size_t max_len,
std::string* string_field) {
ASSERT(NULL != string_field);
ASSERT(pos + max_len <= BLOCK_SIZE);
size_t value_len = talk_base::strlenn(block_ + pos, max_len);
string_field->assign(block_ + pos, value_len);
ASSERT(talk_base::memory_check(block_ + pos + value_len,
0,
max_len - value_len));
pos += max_len;
}