Compare commits
No commits in common. "8040006afe3ab4141812eb752bddd564f52d08eb" and "c7cd44513953a4cc9abdfd3c9844413bcec50540" have entirely different histories.
8040006afe
...
c7cd445139
2 changed files with 45 additions and 332 deletions
371
src/archiver.c
371
src/archiver.c
|
@ -99,6 +99,49 @@ void cleanup_temp_filename_delete(void ***ptrs_array) {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char *filename_to_absolute_path(const char *filename) {
|
||||||
|
#if SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_COSMOPOLITAN || \
|
||||||
|
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_MAC || \
|
||||||
|
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_LINUX
|
||||||
|
__attribute__((cleanup(simple_archiver_helper_cleanup_malloced))) void *path =
|
||||||
|
malloc(strlen(filename) + 1);
|
||||||
|
strncpy(path, filename, strlen(filename) + 1);
|
||||||
|
|
||||||
|
char *path_dir = dirname(path);
|
||||||
|
if (!path_dir) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((
|
||||||
|
cleanup(simple_archiver_helper_cleanup_malloced))) void *dir_realpath =
|
||||||
|
realpath(path_dir, NULL);
|
||||||
|
if (!dir_realpath) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate "path" since it may have been modified by dirname().
|
||||||
|
simple_archiver_helper_cleanup_malloced(&path);
|
||||||
|
path = malloc(strlen(filename) + 1);
|
||||||
|
strncpy(path, filename, strlen(filename) + 1);
|
||||||
|
|
||||||
|
char *filename_basename = basename(path);
|
||||||
|
if (!filename_basename) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get combined full path to file.
|
||||||
|
char *fullpath =
|
||||||
|
malloc(strlen(dir_realpath) + 1 + strlen(filename_basename) + 1);
|
||||||
|
strncpy(fullpath, dir_realpath, strlen(dir_realpath) + 1);
|
||||||
|
fullpath[strlen(dir_realpath)] = '/';
|
||||||
|
strncpy(fullpath + strlen(dir_realpath) + 1, filename_basename,
|
||||||
|
strlen(filename_basename) + 1);
|
||||||
|
|
||||||
|
return fullpath;
|
||||||
|
#endif
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
int write_files_fn(void *data, void *ud) {
|
int write_files_fn(void *data, void *ud) {
|
||||||
const SDArchiverFileInfo *file_info = data;
|
const SDArchiverFileInfo *file_info = data;
|
||||||
SDArchiverState *state = ud;
|
SDArchiverState *state = ud;
|
||||||
|
@ -717,7 +760,7 @@ int write_files_fn(void *data, void *ud) {
|
||||||
// First get absolute path of link.
|
// First get absolute path of link.
|
||||||
__attribute__((cleanup(
|
__attribute__((cleanup(
|
||||||
simple_archiver_helper_cleanup_malloced))) void *link_abs_path =
|
simple_archiver_helper_cleanup_malloced))) void *link_abs_path =
|
||||||
simple_archiver_file_abs_path(file_info->filename);
|
filename_to_absolute_path(file_info->filename);
|
||||||
if (!link_abs_path) {
|
if (!link_abs_path) {
|
||||||
fprintf(stderr, "WARNING: Failed to get absolute path of link!\n");
|
fprintf(stderr, "WARNING: Failed to get absolute path of link!\n");
|
||||||
} else {
|
} else {
|
||||||
|
@ -852,7 +895,7 @@ int filenames_to_abs_map_fn(void *data, void *ud) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get combined full path to file.
|
// Get combined full path to file.
|
||||||
char *fullpath = simple_archiver_file_abs_path(file_info->filename);
|
char *fullpath = filename_to_absolute_path(file_info->filename);
|
||||||
if (!fullpath) {
|
if (!fullpath) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -1325,27 +1368,6 @@ void simple_archiver_internal_cleanup_decomp(pid_t *decomp_pid) {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int symlinks_and_files_from_files(void *data, void *ud) {
|
|
||||||
SDArchiverFileInfo *file_info = data;
|
|
||||||
void **ptr_array = ud;
|
|
||||||
SDArchiverLinkedList *symlinks_list = ptr_array[0];
|
|
||||||
SDArchiverLinkedList *files_list = ptr_array[1];
|
|
||||||
|
|
||||||
if (file_info->filename) {
|
|
||||||
if (file_info->link_dest) {
|
|
||||||
simple_archiver_list_add(
|
|
||||||
symlinks_list, file_info->filename,
|
|
||||||
simple_archiver_helper_datastructure_cleanup_nop);
|
|
||||||
} else {
|
|
||||||
simple_archiver_list_add(
|
|
||||||
files_list, file_info->filename,
|
|
||||||
simple_archiver_helper_datastructure_cleanup_nop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *simple_archiver_error_to_string(enum SDArchiverStateReturns error) {
|
char *simple_archiver_error_to_string(enum SDArchiverStateReturns error) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case SDAS_SUCCESS:
|
case SDAS_SUCCESS:
|
||||||
|
@ -1570,266 +1592,6 @@ int simple_archiver_write_v1(FILE *out_f, SDArchiverState *state,
|
||||||
}
|
}
|
||||||
free(ptr_array);
|
free(ptr_array);
|
||||||
|
|
||||||
// Get a list of symlinks and a list of files.
|
|
||||||
__attribute__((cleanup(simple_archiver_list_free)))
|
|
||||||
SDArchiverLinkedList *symlinks_list = simple_archiver_list_init();
|
|
||||||
__attribute__((cleanup(simple_archiver_list_free)))
|
|
||||||
SDArchiverLinkedList *files_list = simple_archiver_list_init();
|
|
||||||
|
|
||||||
ptr_array = malloc(sizeof(void *) * 2);
|
|
||||||
ptr_array[0] = symlinks_list;
|
|
||||||
ptr_array[1] = files_list;
|
|
||||||
|
|
||||||
if (simple_archiver_list_get(filenames, symlinks_and_files_from_files,
|
|
||||||
ptr_array)) {
|
|
||||||
free(ptr_array);
|
|
||||||
return SDAS_INTERNAL_ERROR;
|
|
||||||
}
|
|
||||||
free(ptr_array);
|
|
||||||
|
|
||||||
if (fwrite("SIMPLE_ARCHIVE_VER", 1, 18, out_f) != 18) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
char buf[1024];
|
|
||||||
uint16_t u16 = 1;
|
|
||||||
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state->parsed->compressor && !state->parsed->decompressor) {
|
|
||||||
return SDAS_NO_DECOMPRESSOR;
|
|
||||||
} else if (!state->parsed->compressor && state->parsed->decompressor) {
|
|
||||||
return SDAS_NO_COMPRESSOR;
|
|
||||||
} else if (state->parsed->compressor && state->parsed->decompressor) {
|
|
||||||
// 4 bytes flags, using de/compressor.
|
|
||||||
memset(buf, 0, 4);
|
|
||||||
buf[0] |= 1;
|
|
||||||
if (fwrite(buf, 1, 4, out_f) != 4) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t len = strlen(state->parsed->compressor);
|
|
||||||
if (len >= 0xFFFF) {
|
|
||||||
fprintf(stderr, "ERROR: Compressor cmd is too long!\n");
|
|
||||||
return SDAS_INVALID_PARSED_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
u16 = (uint16_t)len;
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(&u16, 1, 2, out_f) != 2) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
|
|
||||||
if (fwrite(state->parsed->compressor, 1, u16 + 1, out_f) !=
|
|
||||||
(size_t)u16 + 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
len = strlen(state->parsed->decompressor);
|
|
||||||
if (len >= 0xFFFF) {
|
|
||||||
fprintf(stderr, "ERROR: Decompressor cmd is too long!\n");
|
|
||||||
return SDAS_INVALID_PARSED_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
u16 = (uint16_t)len;
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(&u16, 1, 2, out_f) != 2) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
|
|
||||||
if (fwrite(state->parsed->decompressor, 1, u16 + 1, out_f) !=
|
|
||||||
(size_t)u16 + 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 4 bytes flags, not using de/compressor.
|
|
||||||
memset(buf, 0, 4);
|
|
||||||
if (fwrite(buf, 1, 4, out_f) != 4) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (symlinks_list->count > 0xFFFFFFFF) {
|
|
||||||
fprintf(stderr, "ERROR: Too many symlinks!\n");
|
|
||||||
return SDAS_INVALID_PARSED_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t u32 = (uint32_t)symlinks_list->count;
|
|
||||||
simple_archiver_helper_32_bit_be(&u32);
|
|
||||||
if (fwrite(&u32, 4, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
simple_archiver_helper_32_bit_be(&u32);
|
|
||||||
|
|
||||||
{
|
|
||||||
__attribute__((cleanup(
|
|
||||||
simple_archiver_helper_cleanup_chdir_back))) char *original_cwd = NULL;
|
|
||||||
if (state->parsed->user_cwd) {
|
|
||||||
#if SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_COSMOPOLITAN || \
|
|
||||||
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_MAC || \
|
|
||||||
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_LINUX
|
|
||||||
original_cwd = realpath(".", NULL);
|
|
||||||
if (chdir(state->parsed->user_cwd)) {
|
|
||||||
return SDAS_INTERNAL_ERROR;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
const SDArchiverLLNode *node = symlinks_list->head;
|
|
||||||
for (u32 = 0;
|
|
||||||
u32 < (uint32_t)symlinks_list->count && node != symlinks_list->tail;) {
|
|
||||||
node = node->next;
|
|
||||||
++u32;
|
|
||||||
u16 = 0;
|
|
||||||
#if SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_COSMOPOLITAN || \
|
|
||||||
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_MAC || \
|
|
||||||
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_LINUX
|
|
||||||
// Check if symlink points to thing to be stored into archive.
|
|
||||||
__attribute__((
|
|
||||||
cleanup(simple_archiver_helper_cleanup_malloced))) void *abs_path =
|
|
||||||
realpath(node->data, NULL);
|
|
||||||
__attribute__((cleanup(
|
|
||||||
simple_archiver_helper_cleanup_malloced))) void *rel_path = NULL;
|
|
||||||
if (abs_path) {
|
|
||||||
__attribute__((cleanup(
|
|
||||||
simple_archiver_helper_cleanup_malloced))) void *link_abs_path =
|
|
||||||
simple_archiver_file_abs_path(node->data);
|
|
||||||
if (!link_abs_path) {
|
|
||||||
fprintf(stderr, "WARNING: Failed to get absolute path to link!\n");
|
|
||||||
} else {
|
|
||||||
rel_path = simple_archiver_filenames_to_relative_path(link_abs_path,
|
|
||||||
abs_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (abs_path && (state->parsed->flags & 0x20) == 0 &&
|
|
||||||
!simple_archiver_hash_map_get(abs_filenames, abs_path,
|
|
||||||
strlen(abs_path) + 1)) {
|
|
||||||
// Is not a filename being archived, set preference to absolute path.
|
|
||||||
u16 |= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get symlink stats for permissions.
|
|
||||||
struct stat stat_buf;
|
|
||||||
memset(&stat_buf, 0, sizeof(struct stat));
|
|
||||||
int stat_status =
|
|
||||||
fstatat(AT_FDCWD, node->data, &stat_buf, AT_SYMLINK_NOFOLLOW);
|
|
||||||
if (stat_status != 0) {
|
|
||||||
return SDAS_INTERNAL_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((stat_buf.st_mode & S_IRUSR) != 0) {
|
|
||||||
u16 |= 2;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IWUSR) != 0) {
|
|
||||||
u16 |= 4;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IXUSR) != 0) {
|
|
||||||
u16 |= 8;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IRGRP) != 0) {
|
|
||||||
u16 |= 0x10;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IWGRP) != 0) {
|
|
||||||
u16 |= 0x20;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IXGRP) != 0) {
|
|
||||||
u16 |= 0x40;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IROTH) != 0) {
|
|
||||||
u16 |= 0x80;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IWOTH) != 0) {
|
|
||||||
u16 |= 0x100;
|
|
||||||
}
|
|
||||||
if ((stat_buf.st_mode & S_IXOTH) != 0) {
|
|
||||||
u16 |= 0x200;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
u16 |= 0x3FE;
|
|
||||||
#endif
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t len = strlen(node->data);
|
|
||||||
if (len >= 0xFFFF) {
|
|
||||||
fprintf(stderr, "ERROR: Link name is too long!\n");
|
|
||||||
return SDAS_INVALID_PARSED_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
u16 = (uint16_t)len;
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(node->data, 1, u16 + 1, out_f) != (size_t)u16 + 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abs_path) {
|
|
||||||
len = strlen(abs_path);
|
|
||||||
if (len >= 0xFFFF) {
|
|
||||||
fprintf(stderr,
|
|
||||||
"ERROR: Symlink destination absolute path is too long!\n");
|
|
||||||
return SDAS_INVALID_PARSED_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
u16 = (uint16_t)len;
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(abs_path, 1, u16 + 1, out_f) != (size_t)u16 + 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
u16 = 0;
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rel_path) {
|
|
||||||
len = strlen(rel_path);
|
|
||||||
if (len >= 0xFFFF) {
|
|
||||||
fprintf(stderr,
|
|
||||||
"ERROR: Symlink destination relative path is too long!\n");
|
|
||||||
return SDAS_INVALID_PARSED_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
u16 = (uint16_t)len;
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
simple_archiver_helper_16_bit_be(&u16);
|
|
||||||
if (fwrite(rel_path, 1, u16 + 1, out_f) != (size_t)u16 + 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
u16 = 0;
|
|
||||||
if (fwrite(&u16, 2, 1, out_f) != 1) {
|
|
||||||
return SDAS_FAILED_TO_WRITE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (u32 != (uint32_t)symlinks_list->count) {
|
|
||||||
fprintf(stderr, "ERROR: Iterated through %u symlinks out of %u total!\n",
|
|
||||||
u32, (uint32_t)symlinks_list->count);
|
|
||||||
return SDAS_INTERNAL_ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Chunk count.
|
|
||||||
|
|
||||||
// TODO Impl.
|
// TODO Impl.
|
||||||
fprintf(stderr, "Writing v1 unimplemented\n");
|
fprintf(stderr, "Writing v1 unimplemented\n");
|
||||||
return SDAS_INTERNAL_ERROR;
|
return SDAS_INTERNAL_ERROR;
|
||||||
|
@ -3515,46 +3277,3 @@ char *simple_archiver_filenames_to_relative_path(const char *from_abs,
|
||||||
|
|
||||||
return rel_path;
|
return rel_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *simple_archiver_file_abs_path(const char *filename) {
|
|
||||||
#if SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_COSMOPOLITAN || \
|
|
||||||
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_MAC || \
|
|
||||||
SIMPLE_ARCHIVER_PLATFORM == SIMPLE_ARCHIVER_PLATFORM_LINUX
|
|
||||||
__attribute__((cleanup(simple_archiver_helper_cleanup_malloced))) void *path =
|
|
||||||
malloc(strlen(filename) + 1);
|
|
||||||
strncpy(path, filename, strlen(filename) + 1);
|
|
||||||
|
|
||||||
char *path_dir = dirname(path);
|
|
||||||
if (!path_dir) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((
|
|
||||||
cleanup(simple_archiver_helper_cleanup_malloced))) void *dir_realpath =
|
|
||||||
realpath(path_dir, NULL);
|
|
||||||
if (!dir_realpath) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recreate "path" since it may have been modified by dirname().
|
|
||||||
simple_archiver_helper_cleanup_malloced(&path);
|
|
||||||
path = malloc(strlen(filename) + 1);
|
|
||||||
strncpy(path, filename, strlen(filename) + 1);
|
|
||||||
|
|
||||||
char *filename_basename = basename(path);
|
|
||||||
if (!filename_basename) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get combined full path to file.
|
|
||||||
char *fullpath =
|
|
||||||
malloc(strlen(dir_realpath) + 1 + strlen(filename_basename) + 1);
|
|
||||||
strncpy(fullpath, dir_realpath, strlen(dir_realpath) + 1);
|
|
||||||
fullpath[strlen(dir_realpath)] = '/';
|
|
||||||
strncpy(fullpath + strlen(dir_realpath) + 1, filename_basename,
|
|
||||||
strlen(filename_basename) + 1);
|
|
||||||
|
|
||||||
return fullpath;
|
|
||||||
#endif
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
|
@ -92,10 +92,4 @@ int simple_archiver_de_compress(int pipe_fd_in[2], int pipe_fd_out[2],
|
||||||
char *simple_archiver_filenames_to_relative_path(const char *from_abs,
|
char *simple_archiver_filenames_to_relative_path(const char *from_abs,
|
||||||
const char *to_abs);
|
const char *to_abs);
|
||||||
|
|
||||||
/// Gets the absolute path to a file given a path to a file.
|
|
||||||
/// Should also work on symlinks such that the returned string is the path to
|
|
||||||
/// the link itself, not what it points to.
|
|
||||||
/// Non-NULL on success, and must be free'd if non-NULL.
|
|
||||||
char *simple_archiver_file_abs_path(const char *filename);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in a new issue