#include <config.h>
#include "backupfile.h"
#include "argmatch.h"
#include "dirname.h"
#include "xalloc.h"
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unistd.h>
#include <dirent.h>
#ifndef _D_EXACT_NAMLEN
# define _D_EXACT_NAMLEN(dp) strlen ((dp)->d_name)
#endif
#if D_INO_IN_DIRENT
# define REAL_DIR_ENTRY(dp) ((dp)->d_ino != 0)
#else
# define REAL_DIR_ENTRY(dp) 1
#endif
#if ! (HAVE_PATHCONF && defined _PC_NAME_MAX)
# define pathconf(file, option) (errno = -1)
#endif
#ifndef _POSIX_NAME_MAX
# define _POSIX_NAME_MAX 14
#endif
#ifndef SIZE_MAX
# define SIZE_MAX ((size_t) -1)
#endif
#if defined _XOPEN_NAME_MAX
# define NAME_MAX_MINIMUM _XOPEN_NAME_MAX
#else
# define NAME_MAX_MINIMUM _POSIX_NAME_MAX
#endif
#ifndef HAVE_DOS_FILE_NAMES
# define HAVE_DOS_FILE_NAMES 0
#endif
#ifndef HAVE_LONG_FILE_NAMES
# define HAVE_LONG_FILE_NAMES 0
#endif
#define ISDIGIT(c) ((unsigned int) (c) - '0' <= 9)
#undef opendir
#undef closedir
char const *simple_backup_suffix = "~";
static void
check_extension (char *file, size_t filelen, char e)
{
char *base = last_component (file);
size_t baselen = base_len (base);
size_t baselen_max = HAVE_LONG_FILE_NAMES ? 255 : NAME_MAX_MINIMUM;
if (HAVE_DOS_FILE_NAMES || NAME_MAX_MINIMUM < baselen)
{
long name_max;
char tmp[sizeof "."];
memcpy (tmp, base, sizeof ".");
strcpy (base, ".");
errno = 0;
name_max = pathconf (file, _PC_NAME_MAX);
if (0 <= name_max || errno == 0)
{
long size = baselen_max = name_max;
if (name_max != size)
baselen_max = SIZE_MAX;
}
memcpy (base, tmp, sizeof ".");
}
if (HAVE_DOS_FILE_NAMES && baselen_max <= 12)
{
char *dot = strchr (base, '.');
if (!dot)
baselen_max = 8;
else
{
char const *second_dot = strchr (dot + 1, '.');
baselen_max = (second_dot
? second_dot - base
: dot + 1 - base + 3);
}
}
if (baselen_max < baselen)
{
baselen = file + filelen - base;
if (baselen_max <= baselen)
baselen = baselen_max - 1;
base[baselen] = e;
base[baselen + 1] = '\0';
}
}
enum numbered_backup_result
{
BACKUP_IS_SAME_LENGTH,
BACKUP_IS_LONGER,
BACKUP_IS_NEW
};
static enum numbered_backup_result
numbered_backup (char **buffer, size_t buffer_size, size_t filelen)
{
enum numbered_backup_result result = BACKUP_IS_NEW;
DIR *dirp;
struct dirent *dp;
char *buf = *buffer;
size_t versionlenmax = 1;
char *base = last_component (buf);
size_t base_offset = base - buf;
size_t baselen = base_len (base);
char tmp[sizeof "."];
memcpy (tmp, base, sizeof ".");
strcpy (base, ".");
dirp = opendir (buf);
memcpy (base, tmp, sizeof ".");
strcpy (base + baselen, ".~1~");
if (!dirp)
return result;
while ((dp = readdir (dirp)) != NULL)
{
char const *p;
char *q;
bool all_9s;
size_t versionlen;
size_t new_buflen;
if (! REAL_DIR_ENTRY (dp) || _D_EXACT_NAMLEN (dp) < baselen + 4)
continue;
if (memcmp (buf + base_offset, dp->d_name, baselen + 2) != 0)
continue;
p = dp->d_name + baselen + 2;
if (! ('1' <= *p && *p <= '9'))
continue;
all_9s = (*p == '9');
for (versionlen = 1; ISDIGIT (p[versionlen]); versionlen++)
all_9s &= (p[versionlen] == '9');
if (! (p[versionlen] == '~' && !p[versionlen + 1]
&& (versionlenmax < versionlen
|| (versionlenmax == versionlen
&& memcmp (buf + filelen + 2, p, versionlen) <= 0))))
continue;
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)
{
buf = xnrealloc (buf, 2, new_buflen);
buffer_size = new_buflen * 2;
}
q = buf + filelen;
*q++ = '.';
*q++ = '~';
*q = '0';
q += all_9s;
memcpy (q, p, versionlen + 2);
q += versionlen;
while (*--q == '9')
*q = '0';
++*q;
}
closedir (dirp);
*buffer = buf;
return result;
}
char *
find_backup_file_name (char const *file, enum backup_type backup_type)
{
size_t filelen = strlen (file);
char *s;
size_t ssize;
bool simple = true;
size_t simple_backup_suffix_size = strlen (simple_backup_suffix) + 1;
size_t backup_suffix_size_guess = simple_backup_suffix_size;
enum { GUESS = sizeof ".~12345~" };
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);
if (backup_type != simple_backups)
switch (numbered_backup (&s, ssize, filelen))
{
case BACKUP_IS_SAME_LENGTH:
return s;
case BACKUP_IS_LONGER:
simple = false;
break;
case BACKUP_IS_NEW:
simple = (backup_type == numbered_existing_backups);
break;
}
if (simple)
memcpy (s + filelen, simple_backup_suffix, simple_backup_suffix_size);
check_extension (s, filelen, '~');
return s;
}
static char const * const backup_args[] =
{
"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
};
ARGMATCH_VERIFY (backup_args, backup_types);
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);
}
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"));
}