jak-project/common/util/FileUtil.cpp

522 lines
14 KiB
C++
Raw Normal View History

/*!
* @file FileUtil.cpp
* Utility functions for reading and writing files.
*/
2020-09-09 00:54:16 -04:00
#include "FileUtil.h"
#include <cstdio> /* defines FILENAME_MAX */
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include "BinaryWriter.h"
#include "common/common_types.h"
#include "common/util/BinaryReader.h"
// This disables the use of PCLMULQDQ which is probably ok, but let's just be safe and disable it
// because nobody will care if png compression is 10% slower.
#define FPNG_NO_SSE 1
#include "third-party/fmt/core.h"
#include "third-party/fpng/fpng.cpp"
#include "third-party/fpng/fpng.h"
#include "third-party/lzokay/lzokay.hpp"
2020-09-09 00:54:16 -04:00
2020-09-09 02:41:45 -04:00
#ifdef _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
2020-09-10 06:07:23 -04:00
#include <Windows.h>
2020-09-09 02:41:45 -04:00
#else
#include <cstring>
#include <unistd.h>
2020-09-09 02:41:45 -04:00
#endif
#include "common/util/Assert.h"
#include <common/log/log.h>
2020-09-09 00:54:16 -04:00
namespace file_util {
fs::path get_user_home_dir() {
#ifdef _WIN32
// NOTE - on older systems, this may case issues if it cannot be found!
std::string home_dir = std::getenv("USERPROFILE");
return fs::path(home_dir);
#else
std::string home_dir = std::getenv("HOME");
return fs::path(home_dir);
#endif
}
fs::path get_user_config_dir() {
fs::path config_base_path;
#ifdef _WIN32
auto config_base_dir = std::getenv("APPDATA");
config_base_path = fs::path(std::string(config_base_dir));
#elif __linux
// Docs - https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
// Prefer XDG_CONFIG_HOME if available
auto config_base_dir = std::getenv("XDG_CONFIG_HOME");
if (!config_base_dir) {
config_base_path = get_user_home_dir() / ".config";
} else {
config_base_path = std::string(config_base_dir);
}
#endif
return config_base_path / "OpenGOAL";
}
fs::path get_user_settings_dir() {
// TODO - jak2
return get_user_config_dir() / "jak1" / "settings";
}
fs::path get_user_memcard_dir() {
// TODO - jak2
return get_user_config_dir() / "jak1" / "saves";
}
struct {
bool initialized = false;
fs::path path_to_data;
} gFilePathInfo;
/*!
* Get the path to the current executable.
*/
std::string get_current_executable_path() {
2020-09-10 06:07:23 -04:00
#ifdef _WIN32
char buffer[FILENAME_MAX];
GetModuleFileNameA(NULL, buffer, FILENAME_MAX);
std::string file_path(buffer);
if (file_path.rfind("\\\\?\\", 0) == 0) {
return file_path.substr(4);
}
return file_path;
#else
// do Linux stuff
char buffer[FILENAME_MAX + 1];
auto len = readlink("/proc/self/exe", buffer,
FILENAME_MAX); // /proc/self acts like a "virtual folder" containing
// information about the current process
buffer[len] = '\0';
return std::string(buffer);
#endif
}
/*!
* See if the current executable is somewhere in jak-project/. If so, return the path to jak-project
*/
std::optional<std::string> try_get_jak_project_path() {
std::string my_path = get_current_executable_path();
std::string::size_type pos =
std::string(my_path).rfind("jak-project"); // Strip file path down to /jak-project/ directory
if (pos == std::string::npos) {
return {};
}
return std::make_optional(std::string(my_path).substr(
0, pos + 11)); // + 12 to include "/jak-project" in the returned filepath
}
std::optional<fs::path> try_get_data_dir() {
fs::path my_path = get_current_executable_path();
auto data_dir = my_path.parent_path() / "data";
if (fs::exists(data_dir) && fs::is_directory(data_dir)) {
return std::make_optional(data_dir);
} else {
return {};
}
}
bool setup_project_path(std::optional<fs::path> project_path_override) {
if (gFilePathInfo.initialized) {
return true;
}
if (project_path_override) {
gFilePathInfo.path_to_data = *project_path_override;
gFilePathInfo.initialized = true;
fmt::print("Using explicitly set project path: {}\n", project_path_override->string());
return true;
}
auto data_path = try_get_data_dir();
if (data_path) {
gFilePathInfo.path_to_data = *data_path;
gFilePathInfo.initialized = true;
fmt::print("Using data path: {}\n", data_path->string());
return true;
}
auto development_repo_path = try_get_jak_project_path();
if (development_repo_path) {
gFilePathInfo.path_to_data = *development_repo_path;
gFilePathInfo.initialized = true;
fmt::print("Using development repo path: {}\n", *development_repo_path);
return true;
}
fmt::print("Failed to initialize project path.\n");
return false;
}
fs::path get_jak_project_dir() {
ASSERT(gFilePathInfo.initialized);
return gFilePathInfo.path_to_data;
2020-09-09 02:41:45 -04:00
}
2020-09-09 00:54:16 -04:00
std::string get_file_path(const std::vector<std::string>& input) {
// TODO - clean this behaviour up, it causes unexpected behaviour when working with files
// the project path should be explicitly provided by whatever if needed
// TEMP HACK
// - if the provided path is absolute, don't add the project path
if (input.size() == 1 && fs::path(input.at(0)).is_absolute()) {
return input.at(0);
}
auto current_path = file_util::get_jak_project_dir();
for (auto& str : input) {
current_path /= str;
2020-09-09 18:35:29 -04:00
}
2020-09-09 00:54:16 -04:00
return current_path.string();
2020-09-09 02:41:45 -04:00
}
bool create_dir_if_needed(const fs::path& path) {
if (!fs::is_directory(path)) {
fs::create_directories(path);
return true;
}
return false;
}
bool create_dir_if_needed_for_file(const std::string& path) {
return create_dir_if_needed_for_file(fs::path(path));
}
bool create_dir_if_needed_for_file(const fs::path& path) {
return fs::create_directories(path.parent_path());
}
void write_binary_file(const fs::path& name, const void* data, size_t size) {
FILE* fp = file_util::open_file(name.string().c_str(), "wb");
if (!fp) {
throw std::runtime_error("couldn't open file " + name.string());
}
if (fwrite(data, size, 1, fp) != 1) {
fclose(fp);
throw std::runtime_error("couldn't write file " + name.string());
}
fclose(fp);
}
void write_binary_file(const std::string& name, const void* data, size_t size) {
write_binary_file(fs::path(name), data, size);
}
void write_rgba_png(const fs::path& name, void* data, int w, int h) {
auto flags = 0;
auto ok = fpng::fpng_encode_image_to_file(name.string().c_str(), data, w, h, 4, flags);
if (!ok) {
throw std::runtime_error("couldn't save png file " + name.string());
}
}
void write_text_file(const std::string& file_name, const std::string& text) {
write_text_file(fs::path(file_name), text);
}
void write_text_file(const fs::path& file_name, const std::string& text) {
FILE* fp = file_util::open_file(file_name.string().c_str(), "w");
if (!fp) {
lg::error("Failed to fopen {}\n", file_name.string());
throw std::runtime_error("Failed to open file");
}
fprintf(fp, "%s\n", text.c_str());
fclose(fp);
}
std::vector<uint8_t> read_binary_file(const std::string& filename) {
return read_binary_file(fs::path(filename));
}
std::vector<uint8_t> read_binary_file(const fs::path& path) {
// make sure file exists and isn't a directory
auto status = fs::status(path);
if (!fs::exists(status)) {
throw std::runtime_error(
fmt::format("File {} cannot be opened: does not exist.", path.string()));
}
if (status.type() != fs::file_type::regular && status.type() != fs::file_type::symlink) {
throw std::runtime_error(
fmt::format("File {} cannot be opened: not a regular file or symlink.", path.string()));
}
auto fp = file_util::open_file(path.string().c_str(), "rb");
if (!fp)
throw std::runtime_error("File " + path.string() +
" cannot be opened: " + std::string(strerror(errno)));
fseek(fp, 0, SEEK_END);
auto len = ftell(fp);
rewind(fp);
std::vector<uint8_t> data;
data.resize(len);
if (fread(data.data(), len, 1, fp) != 1) {
fclose(fp);
throw std::runtime_error("File " + path.string() + " cannot be read");
}
fclose(fp);
return data;
}
std::string read_text_file(const fs::path& path) {
std::ifstream file(path.string());
if (!file.good()) {
throw std::runtime_error("couldn't open " + path.string());
}
std::stringstream ss;
ss << file.rdbuf();
return ss.str();
}
std::string read_text_file(const std::string& path) {
return read_text_file(fs::path(path));
}
bool is_printable_char(char c) {
return c >= ' ' && c <= '~';
}
std::string combine_path(const std::string& parent, const std::string& child) {
return parent + "/" + child;
}
bool file_exists(const std::string& path) {
return fs::exists(path);
}
std::string base_name(const std::string& filename) {
size_t pos = 0;
ASSERT(!filename.empty());
for (size_t i = filename.size() - 1; i-- > 0;) {
if (filename.at(i) == '/' || filename.at(i) == '\\') {
pos = (i + 1);
break;
}
}
return filename.substr(pos);
}
void ISONameFromAnimationName(char* dst, const char* src) {
// The Animation Name is a bunch of words separated by dashes
// copy first two chars of the first word exactly
dst[0] = src[0];
dst[1] = src[1];
s32 i = 2; // 2 chars added to dst.
// skip ahead to the first dash (or \0 if there's no dashes)
const char* src_ptr = src;
while (*src_ptr && *src_ptr != '-') {
src_ptr++;
}
// the points to the next dash (or \0 if there's none).
const char* next_ptr = src_ptr;
if (*src_ptr) {
// loop over words (next_ptr points to dash before word, i counts chars in dest)
while (src_ptr = next_ptr + 1, i < 8) {
// scan next_ptr forward to next dash
next_ptr = src_ptr;
while (*next_ptr && *next_ptr != '-') {
next_ptr++;
}
// there's no next word, so break (the current word will be handled there)
if (!*next_ptr)
break;
// add a char for the current word:
char char_to_add;
if (next_ptr[-1] < '0' || next_ptr[-1] > '9') {
// word doesn't end in a number.
// some special case words map to special letters (likely to avoid animation name conflicts)
if (next_ptr - src_ptr == 10 && !memcmp(src_ptr, "resolution", 10)) {
char_to_add = 'z';
} else if (next_ptr - src_ptr == 6 && !memcmp(src_ptr, "accept", 6)) {
char_to_add = 'y';
} else if (next_ptr - src_ptr == 6 && !memcmp(src_ptr, "reject", 6)) {
char_to_add = 'n';
} else {
// not a special case, just take the first letter.
char_to_add = *src_ptr;
}
} else {
// the current word ends in a number, just use this number (I think usually the whole word
// is just a number)
char_to_add = next_ptr[-1];
}
dst[i++] = char_to_add;
}
// here we ran out of room in dest, or words in source.
// if there's still room in dest and chars in source, just add them
while (*src_ptr && (i < 8)) {
dst[i] = *src_ptr;
src_ptr++;
i++;
}
}
// pad with spaces (for ISO Name)
while (i < 8) {
dst[i++] = ' ';
}
// upper case
for (i = 0; i < 8; i++) {
if (dst[i] >= 'a' && dst[i] <= 'z') {
dst[i] -= 0x20;
}
}
// append file extension
strcpy(dst + 8, "STR");
}
void MakeISOName(char* dst, const char* src) {
int i = 0;
const char* src_ptr = src;
char* dst_ptr = dst;
// copy name and upper case
while ((i < 8) && (*src_ptr) && (*src_ptr != '.')) {
char c = *src_ptr;
src_ptr++;
if (('`' < c) && (c < '{')) { // lower case
c -= 0x20;
}
*dst_ptr = c;
dst_ptr++;
i++;
}
// pad out name with spaces
while (i < 8) {
*dst_ptr = ' ';
dst_ptr++;
i++;
}
// increment past period
if (*src_ptr == '.')
src_ptr++;
// same for extension
while (i < 11 && (*src_ptr)) {
char c = *src_ptr;
src_ptr++;
if (('`' < c) && (c < '{')) { // lower case
c -= 0x20;
}
*dst_ptr = c;
dst_ptr++;
i++;
}
while (i < 11) {
*dst_ptr = ' ';
dst_ptr++;
i++;
}
*dst_ptr = 0;
}
void assert_file_exists(const char* path, const char* error_message) {
if (!fs::exists(path)) {
ASSERT_MSG(false, fmt::format("File {} was not found: {}", path, error_message));
}
}
/*!
* Check if the given DGO header (or entire file) is compressed.
*/
bool dgo_header_is_compressed(const std::vector<u8>& data) {
const char compressed_header[] = "oZlB";
bool is_compressed = true;
for (int i = 0; i < 4; i++) {
if (compressed_header[i] != data.at(i)) {
is_compressed = false;
}
}
return is_compressed;
}
/*!
* Decompress a DGO. Resulting data will start at the DGO header.
*/
std::vector<u8> decompress_dgo(const std::vector<u8>& data_in) {
constexpr int MAX_CHUNK_SIZE = 0x8000;
BinaryReader compressed_reader(data_in);
// seek past oZlB
compressed_reader.ffwd(4);
std::size_t decompressed_size = compressed_reader.read<uint32_t>();
std::vector<uint8_t> decompressed_data;
decompressed_data.resize(decompressed_size);
size_t output_offset = 0;
while (true) {
// seek past alignment bytes and read the next chunk size
uint32_t chunk_size = 0;
while (!chunk_size) {
chunk_size = compressed_reader.read<uint32_t>();
}
if (chunk_size < MAX_CHUNK_SIZE) {
std::size_t bytes_written = 0;
lzokay::EResult ok = lzokay::decompress(
compressed_reader.here(), chunk_size, decompressed_data.data() + output_offset,
decompressed_data.size() - output_offset, bytes_written);
ASSERT(ok == lzokay::EResult::Success);
compressed_reader.ffwd(chunk_size);
output_offset += bytes_written;
} else {
// nope - sometimes chunk_size is bigger than MAX, but we should still use max.
// ASSERT(chunk_size == MAX_CHUNK_SIZE);
memcpy(decompressed_data.data() + output_offset, compressed_reader.here(), MAX_CHUNK_SIZE);
compressed_reader.ffwd(MAX_CHUNK_SIZE);
output_offset += MAX_CHUNK_SIZE;
}
if (output_offset >= decompressed_size)
break;
while (compressed_reader.get_seek() % 4) {
compressed_reader.ffwd(1);
}
}
return decompressed_data;
}
FILE* open_file(const fs::path& path, std::string mode) {
#ifdef _WIN32
return _wfopen(path.wstring().c_str(), std::wstring(mode.begin(), mode.end()).c_str());
#else
return fopen(path.string().c_str(), mode.c_str());
#endif
}
} // namespace file_util