diff --git a/NEWS b/NEWS index cbd3f199..64530e72 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,7 @@ User visible changes for UPX Changes in 4.2.0 (XX XXX 2023): * bug fixes - see https://github.com/upx/upx/milestone/13 + * new option '--link' to preserve hard-links (Unix only; use with care) * add support for NO_COLOR env var; see https://no-color.org/ Changes in 4.1.0 (08 Aug 2023): diff --git a/src/conf.h b/src/conf.h index 6f0f77f3..11ff613b 100644 --- a/src/conf.h +++ b/src/conf.h @@ -724,7 +724,7 @@ extern const char *progname; bool main_set_exit_code(int ec); int main_get_options(int argc, char **argv); void main_get_envoptions(); -int upx_main(int argc, char *argv[]); +int upx_main(int argc, char *argv[]) may_throw; // msg.cpp void printSetNl(int need_nl) noexcept; @@ -741,8 +741,8 @@ void infoHeader(); void infoWriting(const char *what, upx_int64_t size); // work.cpp -void do_one_file(const char *iname, char *oname); -int do_files(int i, int argc, char *argv[]); +void do_one_file(const char *iname, char *oname) may_throw; +int do_files(int i, int argc, char *argv[]) may_throw; // help.cpp extern const char gitrev[]; diff --git a/src/file.cpp b/src/file.cpp index b07fcc80..b482c5e5 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -33,6 +33,7 @@ **************************************************************************/ /*static*/ void FileBase::chmod(const char *name, int mode) { + assert(name != nullptr && name[0] != 0); #if HAVE_CHMOD if (::chmod(name, mode) != 0) throwIOException(name, errno); @@ -51,9 +52,16 @@ throwIOException("rename error", errno); } -/*static*/ void FileBase::unlink(const char *name) { - if (::unlink(name) != 0) +/*static*/ bool FileBase::unlink(const char *name, bool check) { + assert(name != nullptr && name[0] != 0); + bool success = ::unlink(name) == 0; +#if HAVE_CHMOD + if (!success) + success = (::chmod(name, 0666) == 0 && ::unlink(name) == 0); +#endif + if (check && !success) throwIOException(name, errno); + return success; } /************************************************************************* @@ -122,14 +130,14 @@ upx_off_t FileBase::seek(upx_off_t off, int whence) { if (off < 0) throwIOException("bad seek 2"); off += _offset; - } - if (whence == SEEK_END) { + } else if (whence == SEEK_END) { if (off > 0) throwIOException("bad seek 3"); off += _offset + _length; whence = SEEK_SET; - } - // SEEK_CUR falls through to here + } else if (whence == SEEK_CUR) { + } else + throwInternalError("bad seek: whence"); upx_off_t l = ::lseek(_fd, off, whence); if (l < 0) throwIOException("seek error", errno); diff --git a/src/file.h b/src/file.h index 1584b947..9eb8d4ef 100644 --- a/src/file.h +++ b/src/file.h @@ -50,9 +50,9 @@ public: public: // static file-related util functions; will throw on error - static void chmod(const char *name, int mode); - static void rename(const char *old_, const char *new_); - static void unlink(const char *name); + static void chmod(const char *name, int mode) may_throw; + static void rename(const char *old_, const char *new_) may_throw; + static bool unlink(const char *name, bool check = true) may_throw; protected: bool do_sopen(); diff --git a/src/help.cpp b/src/help.cpp index 07383630..d9392387 100644 --- a/src/help.cpp +++ b/src/help.cpp @@ -185,7 +185,6 @@ void show_help(int verbose) { con_fprintf(f, " -q be quiet -v be verbose\n" " -oFILE write output to 'FILE'\n" - //" -f force overwrite of output files and compression of suspicious files\n" " -f force compression of suspicious files\n" "%s%s" , (verbose == 0) ? " -k keep backup files\n" : "" @@ -222,6 +221,19 @@ void show_help(int verbose) { " --overlay=skip don't compress a file with an overlay\n" "\n"); fg = con_fg(f, FG_YELLOW); + con_fprintf(f, "File system options:\n"); + fg = con_fg(f, fg); + con_fprintf(f, + " --force-overwrite force overwrite of output files\n" +#if defined(__unix__) && !defined(__MSYS2__) + " --link preserve hard links (Unix only) [USE WITH CARE]\n" + " --no-link do not preserve hard links but rename files [default]\n" +#endif + " --no-mode do not preserve file mode (aka permissions)\n" + " --no-owner do not preserve file ownership\n" + " --no-time do not preserve file timestamp\n" + "\n"); + fg = con_fg(f, FG_YELLOW); con_fprintf(f, "Options for djgpp2/coff:\n"); fg = con_fg(f, fg); con_fprintf(f, diff --git a/src/main.cpp b/src/main.cpp index 87de79cb..a87b19f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -160,7 +160,7 @@ static void check_not_both(bool e1, bool e2, const char *c1, const char *c2) { } } -static void check_options(int i, int argc) { +static void check_and_update_options(int i, int argc) { assert(i <= argc); if (opt->cmd != CMD_COMPRESS) { @@ -179,6 +179,7 @@ static void check_options(int i, int argc) { opt->overlay = opt->COPY_OVERLAY; check_not_both(opt->exact, opt->overlay == opt->STRIP_OVERLAY, "--exact", "--overlay=strip"); + check_not_both(opt->force_overwrite, opt->preserve_link, "--force-overwrite", "--link"); // set default backup option if (opt->backup < 0) @@ -198,6 +199,13 @@ static void check_options(int i, int argc) { e_usage(); } } + +#if defined(__unix__) && !defined(__MSYS2__) +#else + // preserve_link is currently silently ignored on non-Unix platforms + // (we may revisit this decision later if there is some actual use-case) + opt->preserve_link = false; +#endif } /************************************************************************* @@ -521,6 +529,14 @@ static int do_option(int optc, const char *arg) { case 519: opt->no_env = true; break; + case 530: + // NOTE: only use "preserve_link" if you really need it, e.g. it can fail + // with ETXTBSY and other unexpected errors; renaming files is much safer + opt->preserve_link = true; + break; + case 531: + opt->preserve_link = false; + break; case 526: opt->preserve_mode = false; break; @@ -797,8 +813,10 @@ int main_get_options(int argc, char **argv) { {"force", 0, N, 'f'}, // force overwrite of output files {"force-compress", 0, N, 'f'}, // and compression of suspicious files {"force-overwrite", 0x90, N, 529}, // force overwrite of output files + {"link", 0x90, N, 530}, // preserve hard link {"info", 0, N, 'i'}, // info mode {"no-env", 0x10, N, 519}, // no environment var + {"no-link", 0x90, N, 531}, // do not preserve hard link [default] {"no-mode", 0x10, N, 526}, // do not preserve mode (permissions) {"no-owner", 0x10, N, 527}, // do not preserve ownership {"no-progress", 0, N, 516}, // no progress bar @@ -1147,7 +1165,7 @@ static void first_options(int argc, char **argv) { // main entry point **************************************************************************/ -int upx_main(int argc, char *argv[]) { +int upx_main(int argc, char *argv[]) may_throw { int i; static char default_argv0[] = "upx"; assert(argc >= 1); // sanity check @@ -1251,7 +1269,7 @@ int upx_main(int argc, char *argv[]) { if (argc == 1) e_help(); set_term(stderr); - check_options(i, argc); + check_and_update_options(i, argc); int num_files = argc - i; if (num_files < 1) { if (opt->verbose >= 2) diff --git a/src/options.h b/src/options.h index 8aa3863e..ddd18374 100644 --- a/src/options.h +++ b/src/options.h @@ -81,6 +81,7 @@ struct Options final { bool no_env; bool no_progress; const char *output_name; + bool preserve_link; bool preserve_mode; bool preserve_ownership; bool preserve_timestamp; diff --git a/src/work.cpp b/src/work.cpp index 18f323f4..32e2b478 100644 --- a/src/work.cpp +++ b/src/work.cpp @@ -34,6 +34,7 @@ #include "file.h" #include "packmast.h" #include "ui.h" +#include "util/membuffer.h" #if (ACC_OS_DOS32) && defined(__DJGPP__) #define USE_FTIME 1 @@ -50,16 +51,94 @@ #define SH_DENYWR (-1) #endif +/************************************************************************* +// util +**************************************************************************/ + // ignore errors in some cases and silence __attribute__((__warn_unused_result__)) #define IGNORE_ERROR(var) ACC_UNUSED(var) +enum WronlyOpenMode { WOM_MUST_EXIST_TRUNCATE, WOM_MUST_CREATE, WOM_CREATE_OR_TRUNCATE }; + +static constexpr int get_wronly_open_flags(WronlyOpenMode mode) noexcept { + constexpr int flags = O_WRONLY | O_BINARY; + if (mode == WOM_MUST_EXIST_TRUNCATE) + return flags | O_TRUNC; // will cause an error if file does not exist + if (mode == WOM_MUST_CREATE) + return flags | O_CREAT | O_EXCL; // will cause an error if file already exists + // create if not exists, otherwise truncate + return flags | O_CREAT | O_TRUNC; +} + +static void copy_file_contents(const char *iname, const char *oname, WronlyOpenMode mode) + may_throw { + InputFile fi; + fi.sopen(iname, O_RDONLY | O_BINARY, SH_DENYWR); + fi.seek(0, SEEK_SET); + int flags = get_wronly_open_flags(mode); + int shmode = SH_DENYWR; + int omode = 0600; // affected by umask; ignored unless O_CREAT + OutputFile fo; + fo.sopen(oname, flags, shmode, omode); + fo.seek(0, SEEK_SET); + MemBuffer buf(256 * 1024 * 1024); + for (;;) { + size_t bytes = fi.read(buf, buf.getSize()); + if (bytes == 0) + break; + fo.write(buf, bytes); + } + fi.closex(); + fo.closex(); +} + +static void copy_file_attributes(const struct stat *st, const char *oname, bool preserve_mode, + bool preserve_ownership, bool preserve_timestamp) noexcept { +#if USE_UTIME + // copy time stamp + if (preserve_timestamp) { + struct utimbuf u; + u.actime = st->st_atime; + u.modtime = st->st_mtime; + int r = utime(oname, &u); + IGNORE_ERROR(r); + } +#endif +#if HAVE_CHOWN + // copy the group ownership + if (preserve_ownership) { + int r = chown(oname, -1, st->st_gid); + IGNORE_ERROR(r); + } +#endif +#if HAVE_CHMOD + // copy permissions + if (preserve_mode) { + int r = chmod(oname, st->st_mode); + IGNORE_ERROR(r); + } +#endif +#if HAVE_CHOWN + // copy the user ownership + if (preserve_ownership) { + int r = chown(oname, st->st_uid, -1); + IGNORE_ERROR(r); + } +#endif + // maybe unused + UNUSED(oname); + UNUSED(preserve_mode); + UNUSED(preserve_ownership); + UNUSED(preserve_timestamp); +} + /************************************************************************* // process one file **************************************************************************/ -void do_one_file(const char *iname, char *oname) { +void do_one_file(const char *const iname, char *const oname) may_throw { int r; - struct stat st; + struct stat st; // stat of iname mem_clear(&st); #if HAVE_LSTAT r = lstat(iname, &st); @@ -99,6 +178,7 @@ void do_one_file(const char *iname, char *oname) { throwIOException("file is write protected -- skipped"); } + // open input file InputFile fi; fi.sopen(iname, O_RDONLY | O_BINARY, SH_DENYWR); @@ -113,6 +193,7 @@ void do_one_file(const char *iname, char *oname) { // open output file OutputFile fo; + bool copy_timestamp_only = false; if (opt->cmd == CMD_COMPRESS || opt->cmd == CMD_DECOMPRESS) { if (opt->to_stdout) { if (!fo.openStdout(1, opt->force ? true : false)) @@ -121,33 +202,30 @@ void do_one_file(const char *iname, char *oname) { char tname[ACC_FN_PATH_MAX + 1]; if (opt->output_name) { strcpy(tname, opt->output_name); - if (opt->force_overwrite || opt->force >= 2) { -#if HAVE_CHMOD - r = chmod(tname, 0777); - IGNORE_ERROR(r); -#endif - r = unlink(tname); - IGNORE_ERROR(r); - } + if ((opt->force_overwrite || opt->force >= 2) && !opt->preserve_link) + FileBase::unlink(tname, false); } else { if (!maketempname(tname, sizeof(tname), iname, ".upx")) throwIOException("could not create a temporary file name"); } - int flags = O_CREAT | O_WRONLY | O_BINARY; - if (opt->force_overwrite || opt->force) - flags |= O_TRUNC; - else - flags |= O_EXCL; + int flags = get_wronly_open_flags(WOM_MUST_CREATE); + if (opt->output_name && opt->preserve_link) { + flags = get_wronly_open_flags(WOM_CREATE_OR_TRUNCATE); + if (file_exists(opt->output_name)) { + flags = get_wronly_open_flags(WOM_MUST_EXIST_TRUNCATE); + copy_timestamp_only = true; + } + } else if (opt->force_overwrite || opt->force) + flags = get_wronly_open_flags(WOM_CREATE_OR_TRUNCATE); int shmode = SH_DENYWR; #if (ACC_ARCH_M68K && ACC_OS_TOS && ACC_CC_GNUC) && defined(__MINT__) + // TODO later: check current mintlib if this hack is still needed flags |= O_TRUNC; shmode = O_DENYRW; #endif // cannot rely on open() because of umask // int omode = st.st_mode | 0600; - int omode = 0600; - if (!opt->preserve_mode) - omode = 0666; + int omode = opt->preserve_mode ? 0600 : 0666; // affected by umask; only for O_CREAT fo.sopen(tname, flags, shmode, omode); // open succeeded - now set oname[] strcpy(oname, tname); @@ -187,59 +265,45 @@ void do_one_file(const char *iname, char *oname) { fo.closex(); fi.closex(); - // rename or delete files + // rename or copy files + // NOTE: only use "preserve_link" if you really need it, e.g. it can fail + // with ETXTBSY and other unexpected errors; renaming files is much safer if (oname[0] && !opt->output_name) { + // both iname and oname do exist; rename oname to iname if (opt->backup) { char bakname[ACC_FN_PATH_MAX + 1]; if (!makebakname(bakname, sizeof(bakname), iname)) throwIOException("could not create a backup file name"); - FileBase::rename(iname, bakname); + if (opt->preserve_link) { + copy_file_contents(iname, bakname, WOM_MUST_CREATE); + copy_file_attributes(&st, bakname, true, true, true); + copy_file_contents(oname, iname, WOM_MUST_EXIST_TRUNCATE); + FileBase::unlink(oname); + copy_timestamp_only = true; + } else { + FileBase::rename(iname, bakname); + FileBase::rename(oname, iname); + } + } else if (opt->preserve_link) { + copy_file_contents(oname, iname, WOM_MUST_EXIST_TRUNCATE); + FileBase::unlink(oname); + copy_timestamp_only = true; } else { -#if HAVE_CHMOD - r = chmod(iname, 0777); - IGNORE_ERROR(r); -#endif FileBase::unlink(iname); + FileBase::rename(oname, iname); } - FileBase::rename(oname, iname); + // now iname is the new packed/unpacked file and oname does not exist any longer } // copy file attributes if (oname[0]) { oname[0] = 0; // done with oname const char *name = opt->output_name ? opt->output_name : iname; - UNUSED(name); -#if USE_UTIME - // copy time stamp - if (opt->preserve_timestamp) { - struct utimbuf u; - u.actime = st.st_atime; - u.modtime = st.st_mtime; - r = utime(name, &u); - IGNORE_ERROR(r); - } -#endif -#if HAVE_CHOWN - // copy the group ownership - if (opt->preserve_ownership) { - r = chown(name, -1, st.st_gid); - IGNORE_ERROR(r); - } -#endif -#if HAVE_CHMOD - // copy permissions - if (opt->preserve_mode) { - r = chmod(name, st.st_mode); - IGNORE_ERROR(r); - } -#endif -#if HAVE_CHOWN - // copy the user ownership - if (opt->preserve_ownership) { - r = chown(name, st.st_uid, -1); - IGNORE_ERROR(r); - } -#endif + if (copy_timestamp_only) + copy_file_attributes(&st, name, false, false, opt->preserve_timestamp); + else + copy_file_attributes(&st, name, opt->preserve_mode, opt->preserve_ownership, + opt->preserve_timestamp); } UiPacker::uiConfirmUpdate(); @@ -251,17 +315,12 @@ void do_one_file(const char *iname, char *oname) { static void unlink_ofile(char *oname) noexcept { if (oname && oname[0]) { -#if HAVE_CHMOD - int r; - r = chmod(oname, 0777); - IGNORE_ERROR(r); -#endif - if (unlink(oname) == 0) - oname[0] = 0; // done with oname + FileBase::unlink(oname, false); + oname[0] = 0; // done with oname } } -int do_files(int i, int argc, char *argv[]) { +int do_files(int i, int argc, char *argv[]) may_throw { upx_compiler_sanity_check(); if (opt->verbose >= 1) { show_header(); @@ -271,7 +330,7 @@ int do_files(int i, int argc, char *argv[]) { for (; i < argc; i++) { infoHeader(); - const char *iname = argv[i]; + const char *const iname = argv[i]; char oname[ACC_FN_PATH_MAX + 1]; oname[0] = 0;