It is like backupfile, except it avoids some race conditions, and it does not output to stderr or exit. * MODULES.html.sh: Add backup-rename. * lib/backup-find.c, lib/backup-internal.h, lib/backup-rename.c: * modules/backup-rename: New files. * lib/backupfile.c: Turn this into an internals file, which contains code common to backupfile and backup_rename. Do not include argmatch.h or xalloc.h: include xalloc-oversized.h. Include renameat2.h and fcntl.h. (BACKUP_NOMEM): New constant. (numbered_backup): New args BASE_OFFSET and *DIRPP. Do not exit on memory exhaustion; just return BACKUP_NOMEM. Caller changed. (backupfile_internal): Rename from find_backup_file_name. Support new arg RENAME. (backup_args, backup_types, get_version, xget_version): Move to lib/backup-find.c. * lib/backupfile.h (backup_file_rename): New decl. * modules/backupfile (Files): Add lib/backup-internal.h, lib/backup-find.c. (Depends-on): Add dirfd, fcntl, renameat2. (lib_SOURCES): Add backup-find.c. --- ChangeLog | 24 ++++++ MODULES.html.sh | 1 + lib/backup-find.c | 91 +++++++++++++++++++++ lib/backup-internal.h | 3 + lib/backup-rename.c | 31 ++++++++ lib/backupfile.c | 213 ++++++++++++++++++++++++++------------------------ lib/backupfile.h | 1 + modules/backup-rename | 38 +++++++++ modules/backupfile | 12 ++- 9 files changed, 308 insertions(+), 106 deletions(-) create mode 100644 lib/backup-find.c create mode 100644 lib/backup-internal.h create mode 100644 lib/backup-rename.c create mode 100644 modules/backup-rename
diff --git a/ChangeLog b/ChangeLog index ce7c46b..a6909f6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,29 @@ 2017-07-30 Paul Eggert <egg...@cs.ucla.edu> + backup-rename: new module + It is like backupfile, except it avoids some race conditions, + and it does not output to stderr or exit. + * MODULES.html.sh: Add backup-rename. + * lib/backup-find.c, lib/backup-internal.h, lib/backup-rename.c: + * modules/backup-rename: New files. + * lib/backupfile.c: Turn this into an internals file, which + contains code common to backupfile and backup_rename. Include + backupfile-internal.h instead of backupfile.h. Do not include + argmatch.h or xalloc.h: include xalloc-oversized.h. Include + renameat2.h and fcntl.h. + (BACKUP_NOMEM): New constant. + (numbered_backup): New args BASE_OFFSET and *DIRPP. Do not exit + on memory exhaustion; just return BACKUP_NOMEM. Caller changed. + (backupfile_internal): Rename from find_backup_file_name. + Support new arg RENAME. + (backup_args, backup_types, get_version, xget_version): + Move to lib/backup-find.c. + * lib/backupfile.h (backup_file_rename): New decl. + * modules/backupfile (Files): Add lib/backup-internal.h, + lib/backup-find.c. + (Depends-on): Add dirfd, fcntl, renameat2. + (lib_SOURCES): Add backup-find.c. + renameat2: port better to older Solaris * lib/renameat2.c (renameat2): Set ret_val properly on old Solaris. Add goto to use a label, to silence picky compilers. diff --git a/MODULES.html.sh b/MODULES.html.sh index 102223d..a64b999 100755 --- a/MODULES.html.sh +++ b/MODULES.html.sh @@ -2637,6 +2637,7 @@ func_all_modules () func_module areadlinkat func_module areadlinkat-with-size func_module backupfile + func_module backup-rename func_module canonicalize func_module canonicalize-lgpl func_module chdir-safer diff --git a/lib/backup-find.c b/lib/backup-find.c new file mode 100644 index 0000000..fbfb54e --- /dev/null +++ b/lib/backup-find.c @@ -0,0 +1,91 @@ +/* backupfile.c -- make Emacs style backup file names + + Copyright 2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include "backup-internal.h" + +#include "argmatch.h" +#include "xalloc.h" + +#include <stdlib.h> + +/* Return the name of a backup file for the existing file FILE, + allocated with malloc. Report an error and exit if out of memory. + Do not call this function if backup_type == no_backups. */ + +char * +find_backup_file_name (char const *file, enum backup_type backup_type) +{ + char *result = backupfile_internal (file, backup_type, false); + if (!result) + xalloc_die (); + return result; +} + +static char const *const backup_args[] = +{ + /* In a series of synonyms, present the most meaningful first, so + that argmatch_valid be more readable. */ + "none", "off", + "simple", "never", + "existing", "nil", + "numbered", "t", + NULL +}; + +static const enum backup_type backup_types[] = +{ + no_backups, no_backups, + simple_backups, simple_backups, + numbered_existing_backups, numbered_existing_backups, + numbered_backups, numbered_backups +}; + +/* Ensure that these two vectors have the same number of elements, + not counting the final NULL in the first one. */ +ARGMATCH_VERIFY (backup_args, backup_types); + +/* Return the type of backup specified by VERSION. + If VERSION is NULL or the empty string, return numbered_existing_backups. + If VERSION is invalid or ambiguous, fail with a diagnostic appropriate + for the specified CONTEXT. Unambiguous abbreviations are accepted. */ + +enum backup_type +get_version (char const *context, char const *version) +{ + if (version == 0 || *version == 0) + return numbered_existing_backups; + else + return XARGMATCH (context, version, backup_args, backup_types); +} + + +/* Return the type of backup specified by VERSION. + If VERSION is NULL, use the value of the envvar VERSION_CONTROL. + If the specified string is invalid or ambiguous, fail with a diagnostic + appropriate for the specified CONTEXT. + Unambiguous abbreviations are accepted. */ + +enum backup_type +xget_version (char const *context, char const *version) +{ + if (version && *version) + return get_version (context, version); + else + return get_version ("$VERSION_CONTROL", getenv ("VERSION_CONTROL")); +} diff --git a/lib/backup-internal.h b/lib/backup-internal.h new file mode 100644 index 0000000..2401c07 --- /dev/null +++ b/lib/backup-internal.h @@ -0,0 +1,3 @@ +#include "backupfile.h" +#include <stdbool.h> +extern char *backupfile_internal (char const *, enum backup_type, bool); diff --git a/lib/backup-rename.c b/lib/backup-rename.c new file mode 100644 index 0000000..628542b --- /dev/null +++ b/lib/backup-rename.c @@ -0,0 +1,31 @@ +/* Rename a file to a backup name, Emacs style. + + Copyright 2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. */ + +#include <config.h> + +#include "backup-internal.h" + +/* Rename the existing file FILE to a backup name, allocated with + malloc, and return the backup name. On failure return a null + pointer, setting errno. Do not call this function if backup_type + == no_backups. */ + +char * +backup_file_rename (char const *file, enum backup_type backup_type) +{ + return backupfile_internal (file, backup_type, true); +} diff --git a/lib/backupfile.c b/lib/backupfile.c index 2d0c750..9773fe2 100644 --- a/lib/backupfile.c +++ b/lib/backupfile.c @@ -20,12 +20,13 @@ #include <config.h> -#include "backupfile.h" +#include "backup-internal.h" -#include "argmatch.h" #include "dirname.h" -#include "xalloc.h" +#include "renameat2.h" +#include "xalloc-oversized.h" +#include <fcntl.h> #include <errno.h> #include <stdbool.h> #include <stdlib.h> @@ -155,7 +156,10 @@ enum numbered_backup_result /* There are no existing backup names. The new name's length should be checked. */ - BACKUP_IS_NEW + BACKUP_IS_NEW, + + /* Memory allocation failure. */ + BACKUP_NOMEM }; /* *BUFFER contains a file name. Store into *BUFFER the next backup @@ -164,33 +168,45 @@ enum numbered_backup_result initial allocated size is BUFFER_SIZE, which must be at least 4 bytes longer than the file name to make room for the initially appended ".~1". FILELEN is the length of the original file name. + BASE_OFFSET is the offset of the basename in *BUFFER. The returned value indicates what kind of backup was found. If an I/O or other read error occurs, use the highest backup number that - was found. */ + was found. + + *DIRPP is the destination directory. If *DIRPP is null, open the + destination directory and store the resulting stream into *DIRPP + without closing the stream. */ static enum numbered_backup_result -numbered_backup (char **buffer, size_t buffer_size, size_t filelen) +numbered_backup (char **buffer, size_t buffer_size, size_t filelen, + ptrdiff_t base_offset, DIR **dirpp) { enum numbered_backup_result result = BACKUP_IS_NEW; - DIR *dirp; + DIR *dirp = *dirpp; struct dirent *dp; char *buf = *buffer; size_t versionlenmax = 1; - char *base = last_component (buf); - size_t base_offset = base - buf; + char *base = buf + base_offset; size_t baselen = base_len (base); - /* Temporarily modify the buffer into its parent directory name, - open the directory, and then restore the buffer. */ - char tmp[sizeof "."]; - memcpy (tmp, base, sizeof "."); - strcpy (base, "."); - dirp = opendir (buf); - memcpy (base, tmp, sizeof "."); - strcpy (base + baselen, ".~1~"); - - if (!dirp) - return result; + if (dirp) + rewinddir (dirp); + else + { + /* Temporarily modify the buffer into its parent directory name, + open the directory, and then restore the buffer. */ + char tmp[sizeof "."]; + memcpy (tmp, base, sizeof "."); + strcpy (base, "."); + dirp = opendir (buf); + if (!dirp && errno == ENOMEM) + result = BACKUP_NOMEM; + memcpy (base, tmp, sizeof "."); + strcpy (base + baselen, ".~1~"); + if (!dirp) + return result; + *dirpp = dirp; + } while ((dp = readdir (dirp)) != NULL) { @@ -198,7 +214,6 @@ numbered_backup (char **buffer, size_t buffer_size, size_t filelen) char *q; bool all_9s; size_t versionlen; - size_t new_buflen; if (! REAL_DIR_ENTRY (dp) || _D_EXACT_NAMLEN (dp) < baselen + 4) continue; @@ -224,17 +239,25 @@ numbered_backup (char **buffer, size_t buffer_size, size_t filelen) && memcmp (buf + filelen + 2, p, versionlen) <= 0)))) continue; - /* This directory has the largest version number seen so far. + /* This entry has the largest version number seen so far. Append this highest numbered extension to the file name, prepending '0' to the number if it is all 9s. */ versionlenmax = all_9s + versionlen; result = (all_9s ? BACKUP_IS_LONGER : BACKUP_IS_SAME_LENGTH); - new_buflen = filelen + 2 + versionlenmax + 1; - if (buffer_size <= new_buflen) + size_t new_buffer_size = filelen + 2 + versionlenmax + 2; + if (buffer_size < new_buffer_size) { - buf = xnrealloc (buf, 2, new_buflen); - buffer_size = new_buflen * 2; + if (! xalloc_oversized (new_buffer_size, 2)) + new_buffer_size *= 2; + char *new_buf = realloc (buf, new_buffer_size); + if (!new_buf) + { + *buffer = buf; + return BACKUP_NOMEM; + } + buf = new_buf; + buffer_size = new_buffer_size; } q = buf + filelen; *q++ = '.'; @@ -251,22 +274,20 @@ numbered_backup (char **buffer, size_t buffer_size, size_t filelen) ++*q; } - closedir (dirp); *buffer = buf; return result; } /* Return the name of the new backup file for the existing file FILE, - allocated with malloc. Report an error and fail if out of memory. + allocated with malloc. If RENAME, also rename FILE to the new name. + On failure, return NULL and set errno. Do not call this function if backup_type == no_backups. */ char * -find_backup_file_name (char const *file, enum backup_type backup_type) +backupfile_internal (char const *file, enum backup_type backup_type, bool rename) { - size_t filelen = strlen (file); - char *s; - size_t ssize; - bool simple = true; + ptrdiff_t base_offset = last_component (file) - file; + size_t filelen = base_offset + strlen (file + base_offset); /* Initialize the default simple backup suffix. */ if (! simple_backup_suffix) @@ -286,80 +307,68 @@ find_backup_file_name (char const *file, enum backup_type backup_type) if (backup_suffix_size_guess < GUESS) backup_suffix_size_guess = GUESS; - ssize = filelen + backup_suffix_size_guess + 1; - s = xmalloc (ssize); - memcpy (s, file, filelen + 1); + ssize_t ssize = filelen + backup_suffix_size_guess + 1; + char *s = malloc (ssize); + if (!s) + return s; - if (backup_type != simple_backups) - switch (numbered_backup (&s, ssize, filelen)) - { - case BACKUP_IS_SAME_LENGTH: - return s; + DIR *dirp = NULL; + while (true) + { + memcpy (s, file, filelen + 1); - case BACKUP_IS_LONGER: - simple = false; + if (backup_type == simple_backups) + memcpy (s + filelen, simple_backup_suffix, simple_backup_suffix_size); + else + switch (numbered_backup (&s, ssize, filelen, base_offset, &dirp)) + { + case BACKUP_IS_SAME_LENGTH: + break; + + case BACKUP_IS_NEW: + if (backup_type == numbered_existing_backups) + { + backup_type = simple_backups; + memcpy (s + filelen, simple_backup_suffix, + simple_backup_suffix_size); + } + check_extension (s, filelen, '~'); + break; + + case BACKUP_IS_LONGER: + check_extension (s, filelen, '~'); + break; + + case BACKUP_NOMEM: + free (s); + errno = ENOMEM; + return NULL; + } + + if (! rename) break; - case BACKUP_IS_NEW: - simple = (backup_type == numbered_existing_backups); + int sdir = dirp ? dirfd (dirp) : -1; + if (sdir < 0) + { + sdir = AT_FDCWD; + base_offset = 0; + } + unsigned flags = backup_type == simple_backups ? 0 : RENAME_NOREPLACE; + if (renameat2 (AT_FDCWD, file, sdir, s + base_offset, flags) == 0) break; - } + int e = errno; + if (e != EEXIST) + { + if (dirp) + closedir (dirp); + free (s); + errno = e; + return NULL; + } + } - if (simple) - memcpy (s + filelen, simple_backup_suffix, simple_backup_suffix_size); - check_extension (s, filelen, '~'); + if (dirp) + closedir (dirp); return s; } - -static char const * const backup_args[] = -{ - /* In a series of synonyms, present the most meaningful first, so - that argmatch_valid be more readable. */ - "none", "off", - "simple", "never", - "existing", "nil", - "numbered", "t", - NULL -}; - -static const enum backup_type backup_types[] = -{ - no_backups, no_backups, - simple_backups, simple_backups, - numbered_existing_backups, numbered_existing_backups, - numbered_backups, numbered_backups -}; - -/* Ensure that these two vectors have the same number of elements, - not counting the final NULL in the first one. */ -ARGMATCH_VERIFY (backup_args, backup_types); - -/* Return the type of backup specified by VERSION. - If VERSION is NULL or the empty string, return numbered_existing_backups. - If VERSION is invalid or ambiguous, fail with a diagnostic appropriate - for the specified CONTEXT. Unambiguous abbreviations are accepted. */ - -enum backup_type -get_version (char const *context, char const *version) -{ - if (version == 0 || *version == 0) - return numbered_existing_backups; - else - return XARGMATCH (context, version, backup_args, backup_types); -} - - -/* Return the type of backup specified by VERSION. - If VERSION is NULL, use the value of the envvar VERSION_CONTROL. - If the specified string is invalid or ambiguous, fail with a diagnostic - appropriate for the specified CONTEXT. - Unambiguous abbreviations are accepted. */ - -enum backup_type -xget_version (char const *context, char const *version) -{ - if (version && *version) - return get_version (context, version); - else - return get_version ("$VERSION_CONTROL", getenv ("VERSION_CONTROL")); -} diff --git a/lib/backupfile.h b/lib/backupfile.h index ccd168c..d578a65 100644 --- a/lib/backupfile.h +++ b/lib/backupfile.h @@ -46,6 +46,7 @@ enum backup_type extern char const *simple_backup_suffix; +char *backup_file_rename (char const *, enum backup_type); char *find_backup_file_name (char const *, enum backup_type); enum backup_type get_version (char const *context, char const *arg); enum backup_type xget_version (char const *context, char const *arg); diff --git a/modules/backup-rename b/modules/backup-rename new file mode 100644 index 0000000..4d8932d --- /dev/null +++ b/modules/backup-rename @@ -0,0 +1,38 @@ +Description: +Rename a file to a backup name. + +Files: +lib/backup-internal.h +lib/backup-rename.c +lib/backupfile.c +lib/backupfile.h +m4/backupfile.m4 + +Depends-on: +argmatch +closedir +d-ino +dirent-safer +dirfd +dirname-lgpl +fcntl +memcmp +opendir +renameat2 +readdir +stdbool + +configure.ac: +gl_BACKUPFILE + +Makefile.am: +lib_SOURCES += backupfile.c backup-rename.c + +Include: +"backupfile.h" + +License: +GPL + +Maintainer: +Paul Eggert, Jim Meyering diff --git a/modules/backupfile b/modules/backupfile index dbd26b6..9e4efd7 100644 --- a/modules/backupfile +++ b/modules/backupfile @@ -1,10 +1,11 @@ Description: -Determination of the filename of a backup file, according to user environment -variables. +Determine the name of a backup file. Files: -lib/backupfile.h +lib/backup-internal.h +lib/backup-find.c lib/backupfile.c +lib/backupfile.h m4/backupfile.m4 Depends-on: @@ -12,9 +13,12 @@ argmatch closedir d-ino dirent-safer +dirfd dirname-lgpl +fcntl memcmp opendir +renameat2 readdir stdbool @@ -22,7 +26,7 @@ configure.ac: gl_BACKUPFILE Makefile.am: -lib_SOURCES += backupfile.c +lib_SOURCES += backupfile.c backup-find.c Include: "backupfile.h" -- 2.7.4