mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
Zip members stored with method 12 are decoded with rbzip2, whole, from the mapping, from what the stream lends or else read, as Zstandard members are. A .bz2 file stands for the file named as it is without the extension: a stream does not say how long it decodes to, so the member is decoded when it is listed, its size and CRC then known, and kept for the extraction that follows. "#" after ".bz2" names its member. HAVE_RBZIP2, on unless configured off, builds both and the decoder; RVZ's bzip2 groups take the decoder alone where it is off. file_archive_whole() is the whole-file read the .zst and .bz2 backends share. archive_bz2_test lists, reads and extracts bzip2 zip members and a .bz2 file, and refuses members whose CRC is wrong.
1093 lines
34 KiB
C
1093 lines
34 KiB
C
/* Copyright (C) 2010-2020 The RetroArch team
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* The following license statement only applies to this file (archive_file.c).
|
|
* ---------------------------------------------------------------------------------------
|
|
*
|
|
* Permission is hereby granted, free of charge,
|
|
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
|
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include <retro_posix_source.h>
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <compat/strl.h>
|
|
#include <file/archive_file.h>
|
|
#include <file/file_path.h>
|
|
#include <streams/file_stream.h>
|
|
#include <retro_miscellaneous.h>
|
|
#include <lists/string_list.h>
|
|
#include <string/stdstring.h>
|
|
|
|
|
|
static int file_archive_get_file_list_cb(
|
|
const char *path,
|
|
const char *valid_exts,
|
|
const uint8_t *cdata,
|
|
unsigned cmode,
|
|
uint32_t csize,
|
|
uint32_t size,
|
|
uint32_t checksum,
|
|
struct archive_extract_userdata *userdata)
|
|
{
|
|
union string_list_elem_attr attr;
|
|
|
|
if (valid_exts)
|
|
{
|
|
size_t _len = strlen(path);
|
|
|
|
/* Reject empty names before the strlen-1 read below: a malformed
|
|
* central directory can carry a zero-length name, which used to
|
|
* index path[SIZE_MAX]. */
|
|
if (_len == 0)
|
|
return 1;
|
|
|
|
/* Checks if this entry is a directory or a file.
|
|
* Skip if directory. */
|
|
if (path[_len - 1] == '/' || path[_len - 1] == '\\')
|
|
return 1;
|
|
|
|
/* The extension list is split once per walk by the caller and
|
|
* carried in userdata->ext - the same field, with the same
|
|
* meaning, that file_archive_extract_cb already reads. It used
|
|
* to be split here instead, which meant one
|
|
* string_list_initialize / string_split_noalloc /
|
|
* string_list_deinitialize cycle per archive member: a pair of
|
|
* heap allocations per entry, for a list that cannot change
|
|
* during the walk.
|
|
*
|
|
* Gate the extension test on the split list but keep the
|
|
* directory skip above gated on valid_exts, so that a split
|
|
* which failed to allocate still skips directories and still
|
|
* falls through to append rather than rejecting everything -
|
|
* exactly what the old inline code did on that path. */
|
|
if (userdata->ext)
|
|
{
|
|
const char *file_ext = path_get_extension(path);
|
|
|
|
if (!file_ext)
|
|
return 1;
|
|
|
|
if (!string_list_find_elem_prefix(userdata->ext, ".", file_ext))
|
|
return -1; /* keep iterating */
|
|
}
|
|
}
|
|
|
|
attr.i = RARCH_COMPRESSED_FILE_IN_ARCHIVE;
|
|
return string_list_append(userdata->list, path, attr);
|
|
}
|
|
|
|
static int file_archive_extract_cb(const char *name, const char *valid_exts,
|
|
const uint8_t *cdata,
|
|
unsigned cmode, uint32_t csize, uint32_t size,
|
|
uint32_t checksum, struct archive_extract_userdata *userdata)
|
|
{
|
|
const char *ext = path_get_extension(name);
|
|
|
|
/* Extract first file that matches our list. */
|
|
if (ext && string_list_find_elem(userdata->ext, ext))
|
|
{
|
|
char new_path[PATH_MAX_LENGTH];
|
|
const char *delim;
|
|
|
|
if ((delim = path_get_archive_delim(userdata->archive_path)))
|
|
{
|
|
if (!string_is_equal_noncase(
|
|
userdata->current_file_path, delim + 1))
|
|
return 1; /* keep searching for the right file */
|
|
}
|
|
|
|
if (userdata->extraction_directory)
|
|
fill_pathname_join_special(new_path, userdata->extraction_directory,
|
|
path_basename(name), sizeof(new_path));
|
|
else
|
|
fill_pathname_resolve_relative(new_path, userdata->archive_path,
|
|
path_basename(name), sizeof(new_path));
|
|
|
|
/* Start it rather than driving it to completion here. If it
|
|
* parks, the iterate loop resumes it on later ticks; the
|
|
* bookkeeping below still has to happen now, because this
|
|
* callback will not be called again for this member. */
|
|
{
|
|
int pret = file_archive_perform_mode_start(new_path,
|
|
valid_exts, cdata, cmode, csize, size,
|
|
checksum, userdata);
|
|
|
|
if (pret != -1)
|
|
{
|
|
userdata->found_file = true;
|
|
userdata->first_extracted_file_path = strdup(new_path);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* The largest archive a 32-bit host maps whole. */
|
|
#define ARCHIVE_MAP_CEILING_32 (256 * 1024 * 1024)
|
|
|
|
/* Ask the VFS to map the archive. A 64-bit host maps it whatever its
|
|
* size, since pages are only faulted in as they are touched; a 32-bit
|
|
* one keeps the mapping to ARCHIVE_MAP_CEILING_32 so a huge archive
|
|
* cannot take the address space the core needs, and reads it through
|
|
* the stream instead. A URL scheme or a failed map just leaves no
|
|
* mapping, which every backend reads through the stream as well. */
|
|
unsigned file_archive_open_hints(int64_t size)
|
|
{
|
|
return (size > 0 && (sizeof(size_t) >= 8
|
|
|| size <= ARCHIVE_MAP_CEILING_32))
|
|
? RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS
|
|
: RETRO_VFS_FILE_ACCESS_HINT_NONE;
|
|
}
|
|
|
|
const uint8_t *file_archive_whole(RFILE *file, int64_t size, void **owned)
|
|
{
|
|
const uint8_t *src = NULL;
|
|
*owned = NULL;
|
|
filestream_seek(file, 0, RETRO_VFS_SEEK_POSITION_START);
|
|
if (filestream_view(file, size, &src) == size)
|
|
return src;
|
|
if (!(*owned = malloc((size_t)size)))
|
|
return NULL;
|
|
filestream_seek(file, 0, RETRO_VFS_SEEK_POSITION_START);
|
|
if (filestream_read(file, *owned, size) != size)
|
|
{
|
|
free(*owned);
|
|
*owned = NULL;
|
|
return NULL;
|
|
}
|
|
return (const uint8_t*)*owned;
|
|
}
|
|
|
|
static int file_archive_parse_file_init(file_archive_transfer_t *state,
|
|
const char *file)
|
|
{
|
|
char path[PATH_MAX_LENGTH];
|
|
char *last = NULL;
|
|
|
|
strlcpy(path, file, sizeof(path));
|
|
|
|
if ((last = (char*)path_get_archive_delim(path)))
|
|
*last = '\0';
|
|
|
|
if (!(state->backend = file_archive_get_file_backend(path)))
|
|
return -1;
|
|
|
|
/* Failed to open archive. */
|
|
if (!(state->archive_file = filestream_open(path,
|
|
RETRO_VFS_FILE_ACCESS_READ,
|
|
file_archive_open_hints(path_get_size(path)))))
|
|
return -1;
|
|
|
|
state->archive_size = filestream_get_size(state->archive_file);
|
|
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
state->archive_mmap_data = NULL;
|
|
{
|
|
int64_t map_len = 0;
|
|
const uint8_t *map = filestream_get_mapped_ptr(state->archive_file, &map_len);
|
|
/* Whole file or nothing: the decoders index it by archive offset. */
|
|
if (map && map_len == state->archive_size && state->archive_size > 0)
|
|
state->archive_mmap_data = (uint8_t*)map;
|
|
}
|
|
#endif
|
|
|
|
state->step_current = 0;
|
|
state->step_total = 0;
|
|
|
|
return state->backend->archive_parse_file_init(state, path);
|
|
}
|
|
|
|
int64_t file_archive_read_at(void *ud, uint64_t off, void *dst, size_t len)
|
|
{
|
|
RFILE *f = (RFILE*)ud;
|
|
uint8_t *out = (uint8_t*)dst;
|
|
size_t done = 0;
|
|
|
|
if ( !f
|
|
|| (int64_t)off < 0
|
|
|| filestream_seek(f, (int64_t)off,
|
|
RETRO_VFS_SEEK_POSITION_START) < 0)
|
|
return -1;
|
|
|
|
while (done < len)
|
|
{
|
|
int64_t r = filestream_read(f, out + done, (int64_t)(len - done));
|
|
if (r < 0)
|
|
return -1;
|
|
if (r == 0)
|
|
break;
|
|
done += (size_t)r;
|
|
}
|
|
|
|
return (int64_t)done;
|
|
}
|
|
|
|
/* Drops the output of a member that will not complete: the temporary
|
|
* file beside the target goes, and the target is left as it was. */
|
|
static void file_archive_pending_abandon(file_archive_transfer_t *state)
|
|
{
|
|
if (state->pending_sink)
|
|
{
|
|
filestream_commit_atomic(state->pending_sink,
|
|
state->pending_path, false);
|
|
state->pending_sink = NULL;
|
|
}
|
|
}
|
|
|
|
void file_archive_parse_file_iterate_stop(file_archive_transfer_t *state)
|
|
{
|
|
if (!state || !state->archive_file)
|
|
return;
|
|
|
|
state->type = ARCHIVE_TRANSFER_DEINIT;
|
|
file_archive_parse_file_iterate(state, NULL, NULL, NULL, NULL, NULL);
|
|
}
|
|
|
|
int file_archive_parse_file_iterate(
|
|
file_archive_transfer_t *state,
|
|
bool *returnerr,
|
|
const char *file,
|
|
const char *valid_exts,
|
|
file_archive_file_cb file_cb,
|
|
struct archive_extract_userdata *userdata)
|
|
{
|
|
if (!state)
|
|
return -1;
|
|
|
|
switch (state->type)
|
|
{
|
|
case ARCHIVE_TRANSFER_NONE:
|
|
break;
|
|
case ARCHIVE_TRANSFER_INIT:
|
|
if (file_archive_parse_file_init(state, file) == 0)
|
|
{
|
|
if (userdata)
|
|
{
|
|
userdata->transfer = state;
|
|
strlcpy(userdata->archive_path, file,
|
|
sizeof(userdata->archive_path));
|
|
}
|
|
state->type = ARCHIVE_TRANSFER_ITERATE;
|
|
}
|
|
else
|
|
{
|
|
state->type = ARCHIVE_TRANSFER_DEINIT_ERROR;
|
|
goto deinit_error;
|
|
}
|
|
break;
|
|
case ARCHIVE_TRANSFER_ITERATE:
|
|
if (state->backend)
|
|
{
|
|
int ret;
|
|
|
|
/* Finish a member parked by an earlier tick before looking
|
|
* at the next entry. This is what turns the backends'
|
|
* "not finished" into a real yield: one slice per call,
|
|
* then straight back to the caller, which for the
|
|
* decompress task is once per frame. */
|
|
if (state->pending_active)
|
|
{
|
|
int pret = file_archive_perform_mode_step(state);
|
|
|
|
if (pret == 0)
|
|
return 0; /* more to do; resume next tick */
|
|
|
|
if (pret == -1)
|
|
{
|
|
state->pending_stop = false;
|
|
state->pending_counted = false;
|
|
state->type = ARCHIVE_TRANSFER_DEINIT_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
/* Finished. Count it, then either honour a stop the
|
|
* scan asked for while this was parked, or come back
|
|
* for the next entry on the following tick so that one
|
|
* call stays one unit of work. */
|
|
if (!state->pending_counted)
|
|
state->step_current++;
|
|
state->pending_counted = false;
|
|
if (state->pending_stop)
|
|
{
|
|
state->pending_stop = false;
|
|
state->type = ARCHIVE_TRANSFER_DEINIT;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
ret = state->backend->archive_parse_file_iterate_step(
|
|
state->context, valid_exts, userdata, file_cb);
|
|
|
|
if (ret == 1)
|
|
{
|
|
state->step_current++; /* found another file */
|
|
/* If that callback parked a decode, the drain branch
|
|
* must not count it a second time. */
|
|
if (state->pending_active)
|
|
state->pending_counted = true;
|
|
}
|
|
if (ret != 1)
|
|
{
|
|
/* A callback that stopped the scan may have parked a
|
|
* member on its way out (extract_cb does exactly that:
|
|
* it starts the decode and returns 0 to stop
|
|
* searching). Tearing down now would drop it, so stay
|
|
* in ITERATE until the pending work has drained; the
|
|
* branch above finishes it and then falls through to
|
|
* here on a later tick with nothing pending. */
|
|
if (state->pending_active)
|
|
state->pending_stop = true;
|
|
else
|
|
state->type = ARCHIVE_TRANSFER_DEINIT;
|
|
}
|
|
if (ret == -1)
|
|
state->type = ARCHIVE_TRANSFER_DEINIT_ERROR;
|
|
|
|
/* early return to prevent deinit from never firing */
|
|
return 0;
|
|
}
|
|
return -1;
|
|
case ARCHIVE_TRANSFER_DEINIT_ERROR:
|
|
deinit_error:
|
|
if (returnerr)
|
|
*returnerr = false;
|
|
case ARCHIVE_TRANSFER_DEINIT:
|
|
/* A member still parked here is being dropped */
|
|
if (state->pending_active)
|
|
{
|
|
state->pending_active = false;
|
|
file_archive_pending_abandon(state);
|
|
}
|
|
|
|
if (state->context)
|
|
{
|
|
if (state->backend->archive_parse_file_free)
|
|
state->backend->archive_parse_file_free(state->context);
|
|
state->context = NULL;
|
|
}
|
|
|
|
if (state->archive_file)
|
|
{
|
|
filestream_close(state->archive_file);
|
|
state->archive_file = NULL;
|
|
}
|
|
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
/* Borrowed from archive_file; filestream_close() above
|
|
* unmapped it. */
|
|
state->archive_mmap_data = NULL;
|
|
#endif
|
|
|
|
if (userdata)
|
|
userdata->transfer = NULL;
|
|
break;
|
|
}
|
|
|
|
if ( state->type == ARCHIVE_TRANSFER_DEINIT ||
|
|
state->type == ARCHIVE_TRANSFER_DEINIT_ERROR)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* file_archive_walk:
|
|
* @file : filename path of archive
|
|
* @valid_exts : Valid extensions of archive to be parsed.
|
|
* If NULL, allow all.
|
|
* @file_cb : file_cb function pointer
|
|
* @userdata : userdata to pass to file_cb function pointer.
|
|
*
|
|
* Low-level file parsing. Enumerates over all files and calls
|
|
* file_cb with userdata.
|
|
*
|
|
* Returns: true (1) on success, otherwise false (0).
|
|
**/
|
|
static bool file_archive_walk(const char *file, const char *valid_exts,
|
|
file_archive_file_cb file_cb, struct archive_extract_userdata *userdata)
|
|
{
|
|
file_archive_transfer_t state;
|
|
bool returnerr = true;
|
|
|
|
/* Zero the whole thing rather than naming each field: the pending
|
|
* decode state added for resumable extraction lives here too, and a
|
|
* stale pending_active read off the stack would send the iterate
|
|
* loop resuming a decode that never started. */
|
|
memset(&state, 0, sizeof(state));
|
|
|
|
state.type = ARCHIVE_TRANSFER_INIT;
|
|
|
|
for (;;)
|
|
{
|
|
if (file_archive_parse_file_iterate(&state, &returnerr, file,
|
|
valid_exts, file_cb, userdata) != 0)
|
|
break;
|
|
}
|
|
|
|
return returnerr;
|
|
}
|
|
|
|
int file_archive_parse_file_progress(file_archive_transfer_t *state)
|
|
{
|
|
if (!state || state->step_total == 0)
|
|
return 0;
|
|
|
|
return (int)((state->step_current * 100) / (state->step_total));
|
|
}
|
|
|
|
/**
|
|
* file_archive_extract_file:
|
|
* @archive_path : filename path to archive.
|
|
* @valid_exts : valid extensions for the file.
|
|
* @extraction_directory : the directory to extract temporary
|
|
* file to.
|
|
*
|
|
* Extract file from archive. If no file inside the archive is
|
|
* specified, the first file found will be used.
|
|
*
|
|
* Returns : true (1) on success, otherwise false (0).
|
|
**/
|
|
bool file_archive_extract_file(
|
|
const char *archive_path,
|
|
const char *valid_exts,
|
|
const char *extraction_directory,
|
|
char *s, size_t len)
|
|
{
|
|
struct archive_extract_userdata userdata;
|
|
struct string_list *list = string_split(valid_exts, "|");
|
|
|
|
userdata.archive_path[0] = '\0';
|
|
userdata.current_file_path[0] = '\0';
|
|
userdata.first_extracted_file_path = NULL;
|
|
userdata.extraction_directory = extraction_directory;
|
|
userdata.ext = list;
|
|
userdata.list = NULL;
|
|
userdata.found_file = false;
|
|
userdata.list_only = false;
|
|
userdata.crc = 0;
|
|
userdata.transfer = NULL;
|
|
userdata.dec = NULL;
|
|
|
|
if ( list
|
|
&& file_archive_walk(archive_path, valid_exts,
|
|
file_archive_extract_cb, &userdata)
|
|
&& userdata.found_file
|
|
)
|
|
{
|
|
if ( userdata.first_extracted_file_path
|
|
&& *userdata.first_extracted_file_path)
|
|
strlcpy(s, userdata.first_extracted_file_path, len);
|
|
/* The success path used to return here without freeing either
|
|
* the split extension list or the path strdup'd by
|
|
* file_archive_extract_cb, so every archive load through this
|
|
* function leaked both. Only the failure path below cleaned
|
|
* up. Fall through to the shared cleanup instead. */
|
|
free(userdata.first_extracted_file_path);
|
|
string_list_free(list);
|
|
return true;
|
|
}
|
|
|
|
if (userdata.first_extracted_file_path)
|
|
free(userdata.first_extracted_file_path);
|
|
if (list)
|
|
string_list_free(list);
|
|
return false;
|
|
}
|
|
|
|
/* Warning: 'list' must zero initialised before
|
|
* calling this function, otherwise memory leaks/
|
|
* undefined behaviour will occur */
|
|
bool file_archive_get_file_list_noalloc(struct string_list *list,
|
|
const char *path,
|
|
const char *valid_exts)
|
|
{
|
|
struct archive_extract_userdata userdata;
|
|
bool ret;
|
|
|
|
if (!list || !string_list_initialize(list))
|
|
return false;
|
|
|
|
strlcpy(userdata.archive_path, path, sizeof(userdata.archive_path));
|
|
userdata.current_file_path[0] = '\0';
|
|
userdata.first_extracted_file_path = NULL;
|
|
userdata.extraction_directory = NULL;
|
|
/* Split the extension list once for the whole walk rather than once
|
|
* per archive member; the listing callback reads it from here. */
|
|
userdata.ext = valid_exts
|
|
? string_split(valid_exts, "|") : NULL;
|
|
userdata.list = list;
|
|
userdata.found_file = false;
|
|
userdata.list_only = true;
|
|
userdata.crc = 0;
|
|
userdata.transfer = NULL;
|
|
userdata.dec = NULL;
|
|
|
|
ret = file_archive_walk(path, valid_exts,
|
|
file_archive_get_file_list_cb, &userdata);
|
|
|
|
if (userdata.ext)
|
|
string_list_free(userdata.ext);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* file_archive_get_file_list:
|
|
* @path : filename path of archive
|
|
*
|
|
* Returns: string listing of files from archive on success, otherwise NULL.
|
|
**/
|
|
struct string_list *file_archive_get_file_list(const char *path,
|
|
const char *valid_exts)
|
|
{
|
|
struct archive_extract_userdata userdata;
|
|
|
|
strlcpy(userdata.archive_path, path, sizeof(userdata.archive_path));
|
|
userdata.current_file_path[0] = '\0';
|
|
userdata.first_extracted_file_path = NULL;
|
|
userdata.extraction_directory = NULL;
|
|
/* Split once for the whole walk; see the twin above. */
|
|
userdata.ext = valid_exts
|
|
? string_split(valid_exts, "|") : NULL;
|
|
userdata.list = string_list_new();
|
|
userdata.found_file = false;
|
|
userdata.list_only = true;
|
|
userdata.crc = 0;
|
|
userdata.transfer = NULL;
|
|
userdata.dec = NULL;
|
|
|
|
if (!userdata.list)
|
|
{
|
|
if (userdata.ext)
|
|
string_list_free(userdata.ext);
|
|
return NULL;
|
|
}
|
|
if (!file_archive_walk(path, valid_exts,
|
|
file_archive_get_file_list_cb, &userdata))
|
|
{
|
|
if (userdata.ext)
|
|
string_list_free(userdata.ext);
|
|
string_list_free(userdata.list);
|
|
return NULL;
|
|
}
|
|
if (userdata.ext)
|
|
string_list_free(userdata.ext);
|
|
return userdata.list;
|
|
}
|
|
|
|
|
|
/* Copies a complete temporary file over a target it could not be
|
|
* renamed onto, and removes it. Only for filesystems that refuse the
|
|
* rename; an atomic replacement is no longer possible there, which is
|
|
* what writing the member in place always meant. */
|
|
static bool file_archive_pending_copy_in_place(const char *temp_path,
|
|
const char *path)
|
|
{
|
|
bool ok = true;
|
|
uint8_t *buf;
|
|
RFILE *in;
|
|
RFILE *out;
|
|
|
|
if (!(buf = (uint8_t*)malloc(65536)))
|
|
return false;
|
|
if (!(in = filestream_open(temp_path, RETRO_VFS_FILE_ACCESS_READ,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE)))
|
|
{
|
|
free(buf);
|
|
return false;
|
|
}
|
|
if (!(out = filestream_open(path, RETRO_VFS_FILE_ACCESS_WRITE,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE)))
|
|
{
|
|
filestream_close(in);
|
|
free(buf);
|
|
return false;
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
int64_t rd = filestream_read(in, buf, 65536);
|
|
if (rd < 0)
|
|
ok = false;
|
|
if (rd <= 0)
|
|
break;
|
|
if (filestream_write(out, buf, rd) != rd)
|
|
{
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
filestream_close(in);
|
|
if (filestream_close(out) != 0)
|
|
ok = false;
|
|
free(buf);
|
|
return ok;
|
|
}
|
|
|
|
/* Puts a completed member in place. Written beside the file and
|
|
* renamed over it: an update stopped part way leaves the old file
|
|
* whole, and whoever has the old file open or mapped keeps reading
|
|
* what was there. Where a file cannot be renamed into place, it is
|
|
* written in place as before. Returns 1 or -1, as the step does. */
|
|
static int file_archive_pending_commit(file_archive_transfer_t *state)
|
|
{
|
|
int ret;
|
|
bool ok = true;
|
|
RFILE *sink = state->pending_sink;
|
|
|
|
state->pending_sink = NULL;
|
|
|
|
/* No temporary file could be opened: the whole member is in hand,
|
|
* so write it as the one-shot path always did */
|
|
if (!sink)
|
|
{
|
|
if (!state->pending_handle.data)
|
|
return -1;
|
|
if ( !filestream_write_file_atomic(state->pending_path,
|
|
state->pending_handle.data, state->pending_size)
|
|
&& !filestream_write_file(state->pending_path,
|
|
state->pending_handle.data, state->pending_size))
|
|
return -1;
|
|
return 1;
|
|
}
|
|
|
|
/* A backend that decoded the whole member at once */
|
|
if (state->pending_handle.data)
|
|
ok = filestream_write(sink, state->pending_handle.data,
|
|
state->pending_size) == (int64_t)state->pending_size;
|
|
|
|
if ((ret = filestream_commit_atomic(sink, state->pending_path, ok)) == 0)
|
|
return 1;
|
|
if (ret == -1)
|
|
return -1;
|
|
|
|
/* Complete, but not renameable into place */
|
|
{
|
|
bool copied = false;
|
|
size_t _len = strlen(state->pending_path);
|
|
char *temp_path = (char*)malloc(_len + sizeof(".tmp"));
|
|
if (!temp_path)
|
|
return -1;
|
|
memcpy(temp_path, state->pending_path, _len);
|
|
memcpy(temp_path + _len, ".tmp", sizeof(".tmp"));
|
|
copied = file_archive_pending_copy_in_place(temp_path,
|
|
state->pending_path);
|
|
filestream_delete(temp_path);
|
|
free(temp_path);
|
|
return copied ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
/* Finish a member whose decode is parked in the transfer, doing one
|
|
* slice of work.
|
|
*
|
|
* Returns 1 when the member is complete and written, 0 when there is
|
|
* more to do and the caller should come back, -1 on failure. The
|
|
* pending slot is cleared on 1 and -1, so the caller never has to. */
|
|
int file_archive_perform_mode_step(file_archive_transfer_t *state)
|
|
{
|
|
int ret;
|
|
|
|
if (!state || !state->pending_active || !state->backend)
|
|
return -1;
|
|
|
|
ret = state->backend->stream_decompress_data_to_file_iterate(
|
|
state->context, &state->pending_handle);
|
|
|
|
if (ret == 0)
|
|
return 0; /* not finished; the caller yields and returns here */
|
|
|
|
state->pending_active = false;
|
|
|
|
if (ret == -1)
|
|
{
|
|
file_archive_pending_abandon(state);
|
|
return -1;
|
|
}
|
|
|
|
return file_archive_pending_commit(state);
|
|
}
|
|
|
|
/* Begin decoding a member into the transfer's pending slot.
|
|
*
|
|
* Returns 1 if it finished immediately, 0 if it parked and wants to be
|
|
* resumed through file_archive_perform_mode_step(), -1 on failure. */
|
|
int file_archive_perform_mode_start(const char *path, const char *valid_exts,
|
|
const uint8_t *cdata, unsigned cmode, uint32_t csize, uint32_t size,
|
|
uint32_t crc32, struct archive_extract_userdata *userdata)
|
|
{
|
|
file_archive_transfer_t *state;
|
|
|
|
if (!userdata || !userdata->transfer || !userdata->transfer->backend)
|
|
return -1;
|
|
|
|
state = userdata->transfer;
|
|
|
|
/* One member at a time. A second start while one is parked would
|
|
* lose the first, so refuse rather than corrupt it. */
|
|
if (state->pending_active)
|
|
return -1;
|
|
|
|
state->pending_handle.data = NULL;
|
|
state->pending_handle.real_checksum = 0;
|
|
|
|
/* The output file comes first: a backend that can stream sees it
|
|
* at init and sizes its buffers for a window, not the member */
|
|
strlcpy(state->pending_path, path, sizeof(state->pending_path));
|
|
state->pending_sink = filestream_open_atomic(path);
|
|
|
|
if (!state->backend->stream_decompress_data_to_file_init(
|
|
state->context, &state->pending_handle,
|
|
cdata, cmode, csize, size))
|
|
{
|
|
file_archive_pending_abandon(state);
|
|
return -1;
|
|
}
|
|
|
|
state->pending_size = size;
|
|
state->pending_active = true;
|
|
|
|
return file_archive_perform_mode_step(state);
|
|
}
|
|
|
|
bool file_archive_perform_mode(const char *path, const char *valid_exts,
|
|
const uint8_t *cdata, unsigned cmode, uint32_t csize, uint32_t size,
|
|
uint32_t crc32, struct archive_extract_userdata *userdata)
|
|
{
|
|
/* The blocking form, for callers that need the whole member before
|
|
* they can continue and have nowhere to yield to. Same work, just
|
|
* driven to completion here instead of across frames. */
|
|
int ret = file_archive_perform_mode_start(path, valid_exts,
|
|
cdata, cmode, csize, size, crc32, userdata);
|
|
|
|
while (ret == 0)
|
|
ret = file_archive_perform_mode_step(userdata->transfer);
|
|
|
|
return ret == 1;
|
|
}
|
|
|
|
/**
|
|
* file_archive_filename_split:
|
|
* @str : filename to turn into a string list
|
|
*
|
|
* Creates a new string list based on filename @path, delimited by a hash (#).
|
|
*
|
|
* Returns: new string list if successful, otherwise NULL.
|
|
*/
|
|
static struct string_list *file_archive_filename_split(const char *path)
|
|
{
|
|
union string_list_elem_attr attr;
|
|
struct string_list *list = string_list_new();
|
|
const char *delim = path_get_archive_delim(path);
|
|
|
|
attr.i = 0;
|
|
|
|
if (delim)
|
|
{
|
|
/* add archive path to list first */
|
|
if (!string_list_append_n(list, path, (unsigned)(delim - path), attr))
|
|
{
|
|
string_list_free(list);
|
|
return NULL;
|
|
}
|
|
|
|
/* now add the path within the archive */
|
|
delim++;
|
|
|
|
if (*delim)
|
|
{
|
|
if (!string_list_append(list, delim, attr))
|
|
{
|
|
string_list_free(list);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!string_list_append(list, path, attr))
|
|
{
|
|
string_list_free(list);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/* Generic compressed file loader.
|
|
* Extracts to buf, unless optional_filename != 0
|
|
* Then extracts to optional_filename and leaves buf alone.
|
|
*/
|
|
int file_archive_compressed_read(
|
|
const char * path, void **buf,
|
|
const char* optional_filename, int64_t *len)
|
|
{
|
|
const struct
|
|
file_archive_file_backend *backend = NULL;
|
|
struct string_list *str_list = NULL;
|
|
|
|
/* Safety check.
|
|
* If optional_filename and optional_filename
|
|
* exists, we simply return 0,
|
|
* hoping that optional_filename is the
|
|
* same as requested.
|
|
*/
|
|
if (optional_filename && path_is_valid(optional_filename))
|
|
{
|
|
*len = 0;
|
|
return 1;
|
|
}
|
|
|
|
if (!(str_list = file_archive_filename_split(path)))
|
|
{
|
|
*len = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* We assure that there is something after the '#' symbol.
|
|
*
|
|
* This error condition happens for example, when
|
|
* path = /path/to/file.7z, or
|
|
* path = /path/to/file.7z#
|
|
*/
|
|
if (str_list->size <= 1)
|
|
{
|
|
/* could not extract string and substring. */
|
|
string_list_free(str_list);
|
|
*len = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* path_get_archive_delim() accepts every archive extension the
|
|
* tree knows about, while a backend is only present when the
|
|
* matching codec is compiled in, so a path that carries a
|
|
* delimiter can still arrive here with no backend to serve it -
|
|
* a '.zst' entry from a playlist on a build without a Zstandard
|
|
* codec, for instance. Report that as a read failure, which is
|
|
* what every caller already handles. */
|
|
if (!(backend = file_archive_get_file_backend(str_list->elems[0].data)))
|
|
{
|
|
string_list_free(str_list);
|
|
*len = 0;
|
|
return 0;
|
|
}
|
|
|
|
*len = backend->compressed_file_read(str_list->elems[0].data,
|
|
str_list->elems[1].data, buf, optional_filename);
|
|
|
|
string_list_free(str_list);
|
|
|
|
if (*len != -1)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
const struct file_archive_file_backend *file_archive_get_zlib_file_backend(void)
|
|
{
|
|
#if defined(HAVE_ZLIB) || defined(HAVE_COMPRESSION)
|
|
/* The ZIP DEFLATE backend decodes through the built-in inflate when zlib
|
|
* is not present, so it is available whenever compression support is. */
|
|
return &zlib_backend;
|
|
#else
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
const struct file_archive_file_backend *file_archive_get_7z_file_backend(void)
|
|
{
|
|
#ifdef HAVE_7ZIP
|
|
return &sevenzip_backend;
|
|
#else
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
const struct file_archive_file_backend *file_archive_get_zstd_file_backend(void)
|
|
{
|
|
#ifdef HAVE_RZSTD
|
|
return &zstd_backend;
|
|
#else
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
const struct file_archive_file_backend *file_archive_get_rar_file_backend(void)
|
|
{
|
|
#ifdef HAVE_RRAR
|
|
return &rar_backend;
|
|
#else
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
const struct file_archive_file_backend* file_archive_get_file_backend(const char *path)
|
|
{
|
|
#if defined(HAVE_7ZIP) || defined(HAVE_ZLIB) || defined(HAVE_RZSTD) || defined(HAVE_RRAR) || defined(HAVE_RBZIP2) || defined(HAVE_COMPRESSION)
|
|
char newpath[PATH_MAX_LENGTH];
|
|
const char *file_ext = NULL;
|
|
char *last = NULL;
|
|
|
|
strlcpy(newpath, path, sizeof(newpath));
|
|
|
|
if ((last = (char*)path_get_archive_delim(newpath)))
|
|
*last = '\0';
|
|
|
|
file_ext = path_get_extension(newpath);
|
|
|
|
#ifdef HAVE_7ZIP
|
|
if (string_is_equal_noncase(file_ext, "7z"))
|
|
return &sevenzip_backend;
|
|
#endif
|
|
|
|
#if defined(HAVE_ZLIB) || defined(HAVE_COMPRESSION)
|
|
/* ZIP/APK decode via zlib, or via the built-in inflate when zlib is
|
|
* not compiled in. */
|
|
if ( string_is_equal_noncase(file_ext, "zip")
|
|
|| string_is_equal_noncase(file_ext, "apk")
|
|
)
|
|
return &zlib_backend;
|
|
#endif
|
|
|
|
#ifdef HAVE_RZSTD
|
|
if (string_is_equal_noncase(file_ext, "zst"))
|
|
return &zstd_backend;
|
|
#endif
|
|
|
|
#ifdef HAVE_RBZIP2
|
|
if (string_is_equal_noncase(file_ext, "bz2"))
|
|
return &bz2_backend;
|
|
#endif
|
|
|
|
#ifdef HAVE_RRAR
|
|
if (string_is_equal_noncase(file_ext, "rar"))
|
|
return &rar_backend;
|
|
#endif
|
|
#endif
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* file_archive_get_file_crc32:
|
|
* @path : filename path of archive
|
|
*
|
|
* Returns: CRC32 of the specified file in the archive, otherwise 0.
|
|
* If no path within the archive is specified, the first
|
|
* file found inside is used.
|
|
**/
|
|
uint32_t file_archive_get_file_crc32(const char *path)
|
|
{
|
|
uint64_t file_size;
|
|
return file_archive_get_file_crc32_and_size(path, &file_size);
|
|
}
|
|
|
|
/**
|
|
* file_archive_get_file_crc32_and_size:
|
|
* @path : filename path of archive
|
|
* @size : size of the file inside the archive
|
|
*
|
|
* Returns: CRC32 of the specified file in the archive, otherwise 0.
|
|
* If no path within the archive is specified, the first
|
|
* file found inside is used.
|
|
**/
|
|
uint32_t file_archive_get_file_crc32_and_size(const char *path, uint64_t *size)
|
|
{
|
|
file_archive_transfer_t state;
|
|
struct archive_extract_userdata userdata = {0};
|
|
bool returnerr = false;
|
|
bool found = true;
|
|
const char *archive_path = NULL;
|
|
bool contains_compressed = path_contains_compressed_file(path);
|
|
|
|
if (contains_compressed)
|
|
{
|
|
archive_path = path_get_archive_delim(path);
|
|
|
|
/* move pointer right after the delimiter to give us the path */
|
|
if (archive_path)
|
|
archive_path += 1;
|
|
}
|
|
|
|
/* Zeroed wholesale for the same reason as file_archive_walk():
|
|
* the pending decode fields must start clear. */
|
|
memset(&state, 0, sizeof(state));
|
|
|
|
state.type = ARCHIVE_TRANSFER_INIT;
|
|
|
|
/* Initialize and open archive first.
|
|
Sets next state type to ITERATE. */
|
|
file_archive_parse_file_iterate(&state,
|
|
&returnerr, path, NULL, NULL,
|
|
&userdata);
|
|
|
|
for (;;)
|
|
{
|
|
/* Nothing left to look at. Without this the loop spins: the
|
|
* iterate call is skipped once the transfer leaves ITERATE, and
|
|
* the two tests below then read a current_file_path that can no
|
|
* longer change. A member that is not in the archive - or an
|
|
* archive that failed to open at all - hung here rather than
|
|
* returning. */
|
|
if (state.type != ARCHIVE_TRANSFER_ITERATE)
|
|
{
|
|
found = false;
|
|
break;
|
|
}
|
|
|
|
/* Now find the first file in the archive. */
|
|
file_archive_parse_file_iterate(&state,
|
|
&returnerr, path, NULL, NULL,
|
|
&userdata);
|
|
|
|
/* If no path specified within archive, stop after
|
|
* finding the first file.
|
|
*/
|
|
if (!contains_compressed)
|
|
break;
|
|
|
|
/* Stop when the right file in the archive is found. */
|
|
if (archive_path)
|
|
{
|
|
if (string_is_equal(userdata.current_file_path, archive_path))
|
|
break;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
file_archive_parse_file_iterate_stop(&state);
|
|
|
|
/* Report nothing rather than whichever entry the walk stopped on:
|
|
* the caller cannot tell a real checksum from a leftover one, and
|
|
* the scanner would match content against the wrong record. */
|
|
if (!found)
|
|
{
|
|
*size = 0;
|
|
return 0;
|
|
}
|
|
|
|
*size = userdata.size;
|
|
return userdata.crc;
|
|
}
|