#include "File.h" #include "Exception.h" #include #include #include #include #include #include #include #include #include namespace coreutils { File::File(coreutils::ZString fileName, int mode, int authority) { open(fileName, mode, authority); } void File::open(coreutils::ZString fileName, int mode, int authority) { this->fileName = fileName; this->mode = mode; this->authority = authority; buffer = NULL; fd = ::open(fileName.c_str(), mode, authority); if(fd < 0) { std::stringstream temp; temp << "Error opening file " << fileName << "."; throw Exception(temp.str(), __FILE__, __LINE__); } // stat() only after a successful open(), and only used to size the // read buffer - previously this ran BEFORE the open()/fd check with // its return value ignored, so a missing file left the stat struct fully // uninitialized and fed that garbage st_size straight into // setBufferSize()'s realloc(), before the open-failure path ever // got a chance to throw its clean Exception. Found while adding a // static-asset route (GET /assets/{filename}) that legitimately // hits this path on every request for a nonexistent file - a // garbage-sized realloc there could abort/crash the whole // long-running APIServer process (shared with every other live // route) instead of cleanly 500ing just that one request. struct stat status; if (fstat(fd, &status) != 0) { std::stringstream temp; temp << "Error statting file " << fileName << "."; throw Exception(temp.str(), __FILE__, __LINE__); } setBufferSize(status.st_size); } File::~File() { close(fd); free(buffer); } void File::setBufferSize(size_t size) { buffer = (char *)realloc(buffer, size); this->size = size; } int File::read() { size = ::read(fd, buffer, size); setBufferSize(size); return size; } int File::readLine() { setBufferSize(4096); size = 0; int len = ::read(fd, buffer + size, 1); if(len == 0) mEof = true; while(!mEof && (*(buffer + size) != '\n')) { if(*(buffer + size) != '\r') { // setBufferSize(size); ++size; } len = ::read(fd, buffer + size, 1); if(len == 0) mEof = true; } return size; } void File::write(std::string data) { ::write(fd, data.c_str(), data.length()); } void File::write(coreutils::ZString &data) { int len = ::write(fd, data.getData(), data.getLength()); } size_t File::currentSize() { struct stat status; if (fstat(fd, &status) != 0) { std::stringstream temp; temp << "Error statting file " << fileName << "."; throw Exception(temp.str(), __FILE__, __LINE__); } return (size_t)status.st_size; } void File::rotate(int maxBackups) { close(fd); // Resolve through fileName first, not its raw configured spelling - // rename() never follows symlinks for either its source or // destination path. BARANTMail's own "barantmail.log" is a symlink // into /var/log/ on purpose (keeps a large, fast-growing log off // the same filesystem/tree as the source checkout) - renaming that // SYMLINK's own path to ".1" below, then recreating a plain file // at the original (now-empty) path via open()'s O_CREAT, would // silently sever the symlink and start a brand new file right back // in the source tree, undoing that setup the very first time // rotation ever fires on a symlinked path. Confirmed live: exactly // this happened to BARANTMail's real log on this code's first // real trigger (a 23GB pre-existing file, already far past the // configured threshold, rotated on the very next size check after // a fresh process started logging to it). realpath() requires the // target to already exist, which it does here (this object has it // open) - falls back to the configured path unresolved only if // that somehow isn't true (e.g. the target was deleted out from // under it), matching plain (non-symlink) File usage exactly as // before. char resolved[PATH_MAX]; std::string configuredPath(fileName.c_str()); std::string base = realpath(configuredPath.c_str(), resolved) ? std::string(resolved) : configuredPath; // Oldest-first: delete what's about to fall off the end, then // shift each remaining backup up by one, working from the // highest number down so a lower-numbered file is never // clobbered before it's been moved out of the way. std::string oldest = base + "." + std::to_string(maxBackups); ::remove(oldest.c_str()); for (int i = maxBackups - 1; i >= 1; i--) { std::string from = base + "." + std::to_string(i); std::string to = base + "." + std::to_string(i + 1); ::rename(from.c_str(), to.c_str()); } ::rename(base.c_str(), (base + ".1").c_str()); // Reopens via the ORIGINAL (possibly symlinked) fileName, not the // resolved `base` - the symlink itself was never touched above // (only its target was renamed away), so this correctly follows it // again and creates a fresh file at the real target location. open(fileName, mode, authority); } std::string File::asString() { return std::string(buffer, size); } coreutils::ZString File::asZString() { return ZString(buffer, size); } bool File::eof() { return mEof; } }