This allows the misuse checks in save and split via
hashsplit.configuration() to work properly.
Parse git config integers ourselves because --type int doesn't provide
any way to distinguish a bad value from some other, more serious
failure (it just exits with status 128). Wrap strtoimax to handle the
conversion in order to more easily match what git does, since it calls
strtoimax with a base of 0, and so handles octal and hex, even though
it doesn't mention that in git-config(1).
Thanks to Johannes Berg for tracking that down.
lib/bup/_helpers.c | 22 +++++++++++++++++++++
lib/bup/git.py | 47 ++++++++++++++++++++++++++++++++++----------
test/int/test_git.py | 34 +++++++++++++++++++++++++++++++-
3 files changed, 92 insertions(+), 11 deletions(-)
diff --git a/lib/bup/_helpers.c b/lib/bup/_helpers.c
index fc4fe0b4..27941e76 100644
--- a/lib/bup/_helpers.c
+++ b/lib/bup/_helpers.c
@@ -1774,6 +1774,26 @@ static PyObject *bup_limited_vint_pack(PyObject *self, PyObject *args)
return NULL;
}
+static PyObject *
+bup_strtoimax(PyObject *self, PyObject *args)
+{
+ char *str;
+ if (!PyArg_ParseTuple(args, cstr_argf, &str))
+ return NULL;
+ errno = 0;
+ char *end;
+ intmax_t v = strtoimax(str, &end, 0);
+ if (end == str)
+ return Py_BuildValue("(Oi)", Py_None, 0);
+ if (v == INTMAX_MAX && errno == ERANGE)
+ return Py_BuildValue("(Oi)", PyFloat_FromDouble(Py_INFINITY), 0);
+ if (v == INTMAX_MIN && errno == ERANGE)
+ return Py_BuildValue("(Oi)", PyFloat_FromDouble(-Py_INFINITY), 0);
+ if (errno)
+ return PyErr_SetFromErrno(PyExc_IOError);
+ return Py_BuildValue("(On)", BUP_LONGISH_TO_PY(v), end - str);
+}
+
static PyMethodDef helper_methods[] = {
{ "write_sparsely", bup_write_sparsely, METH_VARARGS,
"Write buf excepting zeros at the end. Return trailing zero count." },
@@ -1861,6 +1881,8 @@ static PyMethodDef helper_methods[] = {
{ "vint_encode", bup_vint_encode, METH_VARARGS, "encode an int to vint" },
{ "limited_vint_pack", bup_limited_vint_pack, METH_VARARGS,
"Try to pack vint/vuint/str, throwing OverflowError when unable." },
+ { "strtoimax", bup_strtoimax, METH_VARARGS,
+ "Return value and index of first byte after value as per strtoimax(1)" },
{ NULL, NULL, 0, NULL }, // sentinel
};
diff --git a/lib/bup/git.py b/lib/bup/git.py
index eef14fd6..1200d166 100644
--- a/lib/bup/git.py
+++ b/lib/bup/git.py
@@ -69,32 +69,59 @@ def _raise_git_cmd_error(cmd, status):
def repo_config_file(path):
return os.path.join(path or repo(), b'config')
+
+def parse_git_int(v):
+ """Parse v exactly as git config --type int would."""
+ mag, suffix_i = _helpers.strtoimax(v)
+ if mag == float('+inf'):
+ raise ConfigError(f'--type int value {path_msg(v)} too large')
+ if mag == float('-inf'):
+ raise ConfigError(f'--type int value {path_msg(v)} too small')
+ if mag is None:
+ raise ConfigError(f'unrecognized --type int value {path_msg(v)}')
+ unit = v[suffix_i:]
+ if not unit: return mag
+ if unit in (b'k', b'K'): return mag * 1024
+ if unit in (b'm', b'M'): return mag * 1024 * 1024
+ if unit in (b'g', b'G'): return mag * 1024 * 1024 * 1024
+ raise ConfigError(f'unrecognized --type int value {path_msg(v)}')
+
+
def git_config_get(path, option, *, opttype=None):
+ """Return the git config value for path. Return None if the path
+ does not exist. Return an integer for int, boolean for bool, and
+ bytes otherwise. This is stricter than git in that any
+ opttype='bool' values must be 0, 1, true or false.
+
+ """
+ assert opttype in ('bool', 'int', None)
+ # We don't use --type int/bool because git doesn't currently
+ # provide a way to distinguish a bad value from other errors, and
+ # for bool, we're stricter than git.
cmd = [b'git', b'config', b'--file', path, b'--null']
- if opttype == 'int':
- cmd.extend([b'--int'])
- else:
- assert opttype in ('bool', None)
cmd.extend([b'--get', option])
- cp = subprocess.run(cmd, stdout=subprocess.PIPE)
+ cp = subprocess.run(cmd, stdout=PIPE)
# with --null, git writes out a trailing \0 after the value
r = cp.stdout[:-1]
rc = cp.returncode
if rc == 0:
+ if not r:
+ raise ConfigError(f'no output from {cmd_msg(cmd)}')
if opttype == 'int':
- return int(r)
+ val = parse_git_int(r)
+ if val is not None:
+ return val
+ raise ConfigError(f'got invalid int value {path_msg(r)} from {cmd_msg(cmd)}')
if opttype == 'bool': # any positive int is true for git --bool
- if not r:
- raise ConfigError(f'no output from {cmd_msg(cmd)}')
if r in (b'0', b'false'):
return False
if r in (b'1', b'true'):
return True
- raise ConfigError(f'got invalid value {path_msg(r)} from {cmd_msg(cmd)}')
+ raise ConfigError(f'got invalid bool value {path_msg(r)} from {cmd_msg(cmd)}')
return r
if rc == 1:
return None
- raise GitError('%r returned %d' % (cmd, rc))
+ raise GitError(f'unexpected exit {rc} for {cmd_msg(cmd)}')
def get_commit_items(ref, get_ref):
diff --git a/test/int/test_git.py b/test/int/test_git.py
index 35224be8..d15b0281 100644
--- a/test/int/test_git.py
+++ b/test/int/test_git.py
@@ -469,6 +469,38 @@ def test_midx_close(tmpdir):
# check that we don't have it open anymore
WVPASSEQ(False, b'deleted' in fn)
+
+def test_parse_git_int():
+ parse = git.parse_git_int
+ with raises(ConfigError, match="unrecognized --type int value ''"):
+ parse(b'')
+ with raises(ConfigError, match="unrecognized --type int value x"):
+ parse(b'x')
+ with raises(ConfigError, match="unrecognized --type int value 0.1"):
+ parse(b'0.1')
+ with raises(ConfigError, match="value .* too large"):
+ parse(b'10000000000000000000000000')
+ with raises(ConfigError, match="value .* too small"):
+ parse(b'-10000000000000000000000000')
+ assert 0 == parse(b'0')
+ assert 1 == parse(b'1')
+ assert 1 == parse(b' 1')
+ assert 1024 == parse(b'1k')
+ assert 1024**2 == parse(b'1m')
+ assert 1024**3 == parse(b'1g')
+ assert 1 == parse(b'+1')
+ assert 1024 == parse(b'+1k')
+ assert 1024**2 == parse(b'+1m')
+ assert 1024**3 == parse(b'+1g')
+ assert -1 == parse(b'-1')
+ assert -1024 == parse(b'-1k')
+ assert -1024**2 == parse(b'-1m')
+ assert -1024**3 == parse(b'-1g')
+ with raises(ConfigError, match="unrecognized --type int value 1q"):
+ parse(b'1q')
+ assert 43008 == parse(b'42k')
+
+
def test_config(tmpdir):
cfg_file = os.path.join(os.path.dirname(__file__), 'sample.conf')
no_such_file = os.path.join(os.path.dirname(__file__), 'nosuch.conf')
@@ -484,7 +516,7 @@ def test_config(tmpdir):
WVPASSEQ(git.git_config_get(no_such_file, b'bup.foo'), None)
WVEXCEPT(ConfigError, git_config_get, b'bup.isbad', opttype='bool')
- WVEXCEPT(git.GitError, git_config_get, b'bup.isbad', opttype='int')
+ WVEXCEPT(ConfigError, git_config_get, b'bup.isbad', opttype='int')
WVPASSEQ(git_config_get(b'bup.isbad'), b'ok')
WVPASSEQ(True, git_config_get(b'bup.istrue1', opttype='bool'))
WVPASSEQ(True, git_config_get(b'bup.istrue2', opttype='bool'))
--
2.47.3