diff --git a/rpcs3/Loader/TAR.cpp b/rpcs3/Loader/TAR.cpp index 254bad0e96..933c29bfee 100644 --- a/rpcs3/Loader/TAR.cpp +++ b/rpcs3/Loader/TAR.cpp @@ -83,21 +83,32 @@ fs::file tar_object::get_file(const std::string& path) u64 size = -1; - if (header.name[0] && std::memcmp(header.magic, "ustar", 5) == 0) + std::string filename; + + if (std::memcmp(header.magic, "ustar", 5) == 0) { const std::string_view size_sv{header.size, std::size(header.size)}; size = octal_text_to_u64(size_sv); // Check for overflows and if surpasses file size - if (size + 512 > size && max_size >= size + 512 && max_size - size - 512 >= largest_offset) + if ((header.name[0] || header.prefix[0]) && size + 512 > size && max_size >= size + 512 && max_size - size - 512 >= largest_offset) { // Cache size in native u64 format static_assert(sizeof(size) < sizeof(header.size)); std::memcpy(header.size, &size, 8); - // Save header andd offset - m_map.insert_or_assign(header.name, std::make_pair(largest_offset + 512, header)); + std::string_view prefix_name{header.prefix, std::size(header.prefix)}; + std::string_view name{header.name, std::size(header.name)}; + + prefix_name = prefix_name.substr(0, prefix_name.find_first_of('\0')); + name = name.substr(0, name.find_first_of('\0')); + + filename += prefix_name; + filename += name; + + // Save header and offset + m_map.insert_or_assign(filename, std::make_pair(largest_offset + 512, header)); } else { @@ -113,27 +124,26 @@ fs::file tar_object::get_file(const std::string& path) if (size == umax) { - size = 0; - header.name[0] = '\0'; // Ensure path will not be equal + largest_offset += 512; + continue; } - if (!path.empty() && path == header.name) + // Advance offset to next block + largest_offset += utils::align(size, 512) + 512; + + if (!path.empty() && path == filename) { // Path is equal, read file and advance offset to start of next block std::vector buf(size); if (m_file.read(buf, size)) { - largest_offset += utils::align(size, 512) + 512; return fs::make_stream(std::move(buf)); } - tar_log.error("tar_object::get_file() failed to read file entry %s (size=0x%x)", header.name, size); - size = 0; + tar_log.error("tar_object::get_file() failed to read file entry %s (size=0x%x)", filename, size); + largest_offset -= utils::align(size, 512); } - - // Advance offset to next block - largest_offset += utils::align(size, 512) + 512; } return fs::file(); @@ -170,6 +180,18 @@ bool tar_object::extract(std::string vfs_mp) return false; } + u64 mtime = octal_text_to_u64({header.mtime, std::size(header.mtime)}); + + // Let's use it for optional atime + u64 atime = octal_text_to_u64({header.padding, 12}); + + // This is a fake timestamp, it can be invalid + if (atime == umax) + { + // Set to mtime if not provided + atime = mtime; + } + switch (header.filetype) { case '\0': @@ -189,6 +211,14 @@ bool tar_object::extract(std::string vfs_mp) if (file) { file.write(static_cast>*>(data.get())->obj); + file.close(); + + if (mtime != umax && !fs::utime(result, atime, mtime)) + { + tar_log.error("TAR Loader: fs::utime failed on %s (%s)", result, fs::g_tls_error); + return false; + } + tar_log.notice("TAR Loader: written file %s", name); break; } @@ -206,6 +236,12 @@ bool tar_object::extract(std::string vfs_mp) return false; } + if (mtime != umax && !fs::utime(result, atime, mtime)) + { + tar_log.error("TAR Loader: fs::utime failed on %s (%s)", result, fs::g_tls_error); + return false; + } + break; } @@ -217,11 +253,113 @@ bool tar_object::extract(std::string vfs_mp) return true; } -bool extract_tar(const std::string& file_path, const std::string& dir_path) +std::vector tar_object::save_directory(const std::string& src_dir, std::vector&& init, const process_func& func, std::string full_path) +{ + const std::string& target_path = full_path.empty() ? src_dir : full_path; + + fs::stat_t stat{}; + if (!fs::stat(target_path, stat)) + { + return std::move(init); + } + + u32 count = 0; + + if (stat.is_directory) + { + bool has_items = false; + + for (auto& entry : fs::dir(target_path)) + { + if (entry.name.find_first_not_of('.') == umax) continue; + + init = save_directory(src_dir, std::move(init), func, target_path + '/' + entry.name); + has_items = true; + } + + if (has_items) + { + return std::move(init); + } + } + + auto write_octal = [](char* ptr, u64 i) + { + if (!i) + { + *ptr = '0'; + return; + } + + ptr += utils::aligned_div(std::bit_width(i), 3) - 1; + + for (; i; ptr--, i /= 8) + { + *ptr = static_cast('0' + (i % 8)); + } + }; + + std::string saved_path{target_path.data() + src_dir.size(), target_path.size() - src_dir.size()}; + + const u64 old_size = init.size(); + init.resize(old_size + sizeof(TARHeader)); + + if (!stat.is_directory) + { + fs::file fd(target_path); + + if (func) + { + // Use custom function for file saving if provided + // Allows for example to compress PNG files as JPEG in the TAR itself + if (!func(fd, saved_path, std::move(init))) + { + // Revert (this entry should not be included if func returns false) + init.resize(old_size); + return std::move(init); + } + } + else + { + const u64 old_size2 = init.size(); + init.resize(init.size() + stat.size); + ensure(fd.read(init.data() + old_size2, stat.size) == stat.size); + } + + // Align + init.resize(utils::align(init.size(), 512)); + + fd.close(); + fs::utime(target_path, stat.atime, stat.mtime); + } + + TARHeader header{}; + std::memcpy(header.magic, "ustar ", 6); + + // Prefer saving to name field as much as we can + // If it doesn't fit, save 100 characters at name and 155 characters preceding to it at max + const u64 prefix_size = std::clamp(saved_path.size(), 100, 255) - 100; + std::memcpy(header.prefix, saved_path.data(), prefix_size); + const u64 name_size = std::min(saved_path.size(), 255) - prefix_size; + std::memcpy(header.name, saved_path.data() + prefix_size, name_size); + + write_octal(header.size, stat.is_directory ? 0 : stat.size); + write_octal(header.mtime, stat.mtime); + write_octal(header.padding, stat.atime); + header.filetype = stat.is_directory ? '5' : '0'; + + std::memcpy(init.data() + old_size, &header, sizeof(header)); + return std::move(init); +} + +bool extract_tar(const std::string& file_path, const std::string& dir_path, fs::file file) { tar_log.notice("Extracting '%s' to directory '%s'...", file_path, dir_path); - fs::file file(file_path); + if (!file) + { + file.open(file_path); + } if (!file) { diff --git a/rpcs3/Loader/TAR.h b/rpcs3/Loader/TAR.h index e5bfec4174..3e06e1f630 100644 --- a/rpcs3/Loader/TAR.h +++ b/rpcs3/Loader/TAR.h @@ -14,9 +14,14 @@ struct TARHeader char magic[6]; char dontcare2[82]; char prefix[155]; - char padding[12]; + char padding[12]; // atime for RPCS3 }; +namespace fs +{ + class file; +} + class tar_object { const fs::file& m_file; @@ -33,9 +38,13 @@ public: fs::file get_file(const std::string& path); + using process_func = std::function&&)>; + // Extract all files in archive to destination as VFS // Allow to optionally specify explicit mount point (which may be directory meant for extraction) bool extract(std::string vfs_mp = {}); + + static std::vector save_directory(const std::string& src_dir, std::vector&& init = std::vector{}, const process_func& func = {}, std::string append_path = {}); }; -bool extract_tar(const std::string& file_path, const std::string& dir_path); +bool extract_tar(const std::string& file_path, const std::string& dir_path, fs::file file = {});