Commit 05b43732 authored by bescoto's avatar bescoto

ACL/EA parsing fixes, use new quoting style


git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup@403 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109
parent 2525563c
......@@ -12,6 +12,10 @@ If there is data missing from the destination dir (for instance if a
user mistakenly deletes it), only warn when restoring, instead of
exiting with error.
Fixed bug in EA/ACL restoring, noticed by Greg Freemyer. Also updated
quoting of filenames and extended attributes names to match
forthcoming attr/facl utilities.
New in v0.13.1 (2003/08/08)
---------------------------
......
......@@ -27,6 +27,7 @@
#include <unistd.h>
#include <errno.h>
/* Some of the following code to define major/minor taken from code by
* Jrg Schilling's star archiver.
*/
......@@ -202,7 +203,6 @@ static PyObject *c_make_file_dict(self, args)
return return_val;
}
/* Convert python long into 7 byte string */
static PyObject *long2str(self, args)
PyObject *self;
......@@ -247,12 +247,138 @@ static PyObject *str2long(self, args)
}
/* --------------------------------------------------------------------- *
* This section is still GPL'd, but was copied from the libmisc
* section of getfacl by Andreas Gruenbacher
* <a.gruenbacher@computer.org>. I'm just copying the code to
* preserve quoting compatibility between (get|set)f(acl|attr) and
* rdiff-backup. Taken on 8/24/2003.
* --------------------------------------------------------------------- */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int high_water_alloc(void **buf, size_t *bufsize, size_t newsize)
{
#define CHUNK_SIZE 256
/*
* Goal here is to avoid unnecessary memory allocations by
* using static buffers which only grow when necessary.
* Size is increased in fixed size chunks (CHUNK_SIZE).
*/
if (*bufsize < newsize) {
void *newbuf;
newsize = (newsize + CHUNK_SIZE-1) & ~(CHUNK_SIZE-1);
newbuf = realloc(*buf, newsize);
if (!newbuf)
return 1;
*buf = newbuf;
*bufsize = newsize;
}
return 0;
}
const char *quote(const char *str)
{
static char *quoted_str;
static size_t quoted_str_len;
const unsigned char *s;
char *q;
size_t nonpr;
if (!str)
return str;
for (nonpr = 0, s = (unsigned char *)str; *s != '\0'; s++)
if (!isprint(*s) || isspace(*s) || *s == '\\' || *s == '=')
nonpr++;
if (nonpr == 0)
return str;
if (high_water_alloc((void **)&quoted_str, &quoted_str_len,
nonpr * 3 + 1))
return NULL;
for (s = (unsigned char *)str, q = quoted_str; *s != '\0'; s++) {
if (!isprint(*s) || isspace(*s) || *s == '\\' || *s == '=') {
*q++ = '\\';
*q++ = '0' + ((*s >> 6) );
*q++ = '0' + ((*s >> 3) & 7);
*q++ = '0' + ((*s ) & 7);
} else
*q++ = *s;
}
*q++ = '\0';
return quoted_str;
}
char *unquote(char *str)
{
unsigned char *s, *t;
if (!str)
return str;
for (s = (unsigned char *)str; *s != '\0'; s++)
if (*s == '\\')
break;
if (*s == '\0')
return str;
#define isoctal(c) \
((c) >= '0' && (c) <= '7')
t = s;
do {
if (*s == '\\' &&
isoctal(*(s+1)) && isoctal(*(s+2)) && isoctal(*(s+3))) {
*t++ = ((*(s+1) - '0') << 6) +
((*(s+2) - '0') << 3) +
((*(s+3) - '0') );
s += 3;
} else
*t++ = *s;
} while (*s++ != '\0');
return str;
}
/* ------------- End Gruenbach section --------------------------------- */
/* Translate quote above into python */
static PyObject *acl_quote(PyObject *self, PyObject *args)
{
char *s;
if (!PyArg_ParseTuple(args, "s", &s)) return NULL;
return Py_BuildValue("s", quote(s));
}
/* Translate unquote above into python */
static PyObject *acl_unquote(PyObject *self, PyObject *args)
{
char *s;
if (!PyArg_ParseTuple(args, "s", &s)) return NULL;
return Py_BuildValue("s", unquote(s));
}
/* ------------- Python export lists -------------------------------- */
static PyMethodDef CMethods[] = {
{"make_file_dict", c_make_file_dict, METH_VARARGS,
"Make dictionary from file stat"},
{"long2str", long2str, METH_VARARGS, "Convert python long to 7 byte string"},
{"str2long", str2long, METH_VARARGS, "Convert 7 byte string to python long"},
{"sync", my_sync, METH_VARARGS, "sync buffers to disk"},
{"acl_quote", acl_quote, METH_VARARGS,
"Quote string, escaping non-printables"},
{"acl_unquote", acl_unquote, METH_VARARGS,
"Unquote string, producing original input to quote"},
{NULL, NULL, 0, NULL}
};
......@@ -266,4 +392,3 @@ void initC(void)
NULL, NULL);
PyDict_SetItemString(d, "UnknownFileTypeError", UnknownFileTypeError);
}
......@@ -30,8 +30,7 @@ from __future__ import generators
import base64, errno, re
try: import posix1e
except ImportError: pass
import static, Globals, metadata, connection, rorpiter, log
import static, Globals, metadata, connection, rorpiter, log, C, rpath
class ExtendedAttributes:
"""Hold a file's extended attribute information"""
......@@ -104,12 +103,12 @@ def ea_compare_rps(rp1, rp2):
def EA2Record(ea):
"""Convert ExtendedAttributes object to text record"""
str_list = ['# file: %s' % ea.get_indexpath()]
str_list = ['# file: %s' % C.acl_quote(ea.get_indexpath())]
for (name, val) in ea.attr_dict.iteritems():
if not val: str_list.append(name)
else:
encoded_val = base64.encodestring(val).replace('\n', '')
str_list.append('%s=0s%s' % (name, encoded_val))
str_list.append('%s=0s%s' % (C.acl_quote(name), encoded_val))
return '\n'.join(str_list)+'\n'
def Record2EA(record):
......@@ -120,7 +119,7 @@ def Record2EA(record):
raise metadata.ParsingError("Bad record beginning: " + first[:8])
filename = first[8:]
if filename == '.': index = ()
else: index = tuple(filename.split('/'))
else: index = tuple(C.acl_unquote(filename).split('/'))
ea = ExtendedAttributes(index)
for line in lines:
......@@ -137,27 +136,15 @@ def Record2EA(record):
ea.set(name, base64.decodestring(encoded_val))
return ea
def quote_path(path):
"""Quote a path for use EA/ACL records.
Right now no quoting!!! Change this to reflect the updated
quoting style of getfattr/setfattr when they are changed.
"""
return path
class EAExtractor(metadata.FlatExtractor):
"""Iterate ExtendedAttributes objects from the EA information file"""
record_boundary_regexp = re.compile("\\n# file:")
record_boundary_regexp = re.compile('(?:\\n|^)(# file: (.*?))\\n')
record_to_object = staticmethod(Record2EA)
def get_index_re(self, index):
"""Find start of EA record with given index"""
if not index: indexpath = '.'
else: indexpath = '/'.join(index)
# Right now there is no quoting, due to a bug in
# getfacl/setfacl. Replace later when bug fixed.
return re.compile('(^|\\n)(# file: %s\\n)' % indexpath)
def filename_to_index(self, filename):
"""Convert possibly quoted filename to index tuple"""
if filename == '.': return ()
else: return tuple(C.acl_unquote(filename).split('/'))
class ExtendedAttributesFile(metadata.FlatFile):
"""Store/retrieve EAs from extended_attributes file"""
......@@ -171,7 +158,7 @@ class ExtendedAttributesFile(metadata.FlatFile):
"""Add EA information in ea_iter to rorp_iter"""
collated = rorpiter.CollateIterators(rorp_iter, ea_iter)
for rorp, ea in collated:
assert rorp, (rorp, (ea.index, ea.attr_dict), rest_time)
assert rorp, (rorp, (ea.index, ea.attr_dict), time)
if not ea: ea = ExtendedAttributes(rorp.index)
rorp.set_ea(ea)
yield rorp
......@@ -311,7 +298,7 @@ def acl_compare_rps(rp1, rp2):
def ACL2Record(acl):
"""Convert an AccessControlList object into a text record"""
start = "# file: %s\n%s" % (acl.get_indexpath(), acl.acl_text)
start = "# file: %s\n%s" % (C.acl_quote(acl.get_indexpath()), acl.acl_text)
if not acl.def_acl_text: return start
default_lines = acl.def_acl_text.strip().split('\n')
default_text = '\ndefault:'.join(default_lines)
......@@ -325,7 +312,7 @@ def Record2ACL(record):
raise metadata.ParsingError("Bad record beginning: "+ first_line)
filename = first_line[8:]
if filename == '.': index = ()
else: index = tuple(filename.split('/'))
else: index = tuple(C.acl_unquote(filename).split('/'))
normal_entries = []; default_entries = []
for line in lines:
......@@ -393,3 +380,25 @@ def GetCombinedMetadataIter(rbdir, time, restrict_index = None,
metadata_iter, rbdir, time, restrict_index)
return metadata_iter
def rpath_acl_get(rp):
"""Get acls of given rpath rp.
This overrides a function in the rpath module.
"""
acl = AccessControlList(rp.index)
if not rp.issym(): acl.read_from_rp(rp)
return acl
rpath.acl_get = rpath_acl_get
def rpath_ea_get(rp):
"""Get extended attributes of given rpath
This overrides a function in the rpath module.
"""
ea = ExtendedAttributes(rp.index)
if not rp.issym(): ea.read_from_rp(rp)
return ea
rpath.ea_get = rpath_ea_get
......@@ -122,9 +122,7 @@ def Record2RORP(record_string):
"""
data_dict = {}
for field, data in line_parsing_regexp.findall(record_string):
if field == "File":
if data == ".": index = ()
else: index = tuple(unquote_path(data).split("/"))
if field == "File": index = quoted_filename_to_index(data)
elif field == "Type":
if data == "None": data_dict['type'] = None
else: data_dict['type'] = data
......@@ -174,12 +172,23 @@ def unquote_path(quoted_string):
return two_chars
return re.sub("\\\\n|\\\\\\\\", replacement_func, quoted_string)
def quoted_filename_to_index(quoted_filename):
"""Return tuple index given quoted filename"""
if quoted_filename == '.': return ()
else: return tuple(unquote_path(quoted_filename).split('/'))
class FlatExtractor:
"""Controls iterating objects from flat file"""
# The following two should be set in subclasses
record_boundary_regexp = None # Matches beginning of next record
record_to_object = None # Function that converts text record to object
# Set this in subclass. record_boundary_regexp should match
# beginning of next record. The first group should start at the
# beginning of the record. The second group should contain the
# (possibly quoted) filename.
record_boundary_regexp = None
# Set in subclass to function that converts text record to object
record_to_object = None
def __init__(self, fileobj):
self.fileobj = fileobj # holds file object we are reading from
self.buf = "" # holds the next part of the file
......@@ -187,10 +196,10 @@ class FlatExtractor:
self.blocksize = 32 * 1024
def get_next_pos(self):
"""Return position of next record in buffer"""
"""Return position of next record in buffer, or end pos if none"""
while 1:
m = self.record_boundary_regexp.search(self.buf)
if m: return m.start(0)+1 # the +1 skips the newline
m = self.record_boundary_regexp.search(self.buf, 1)
if m: return m.start(1)
else: # add next block to the buffer, loop again
newbuf = self.fileobj.read(self.blocksize)
if not newbuf:
......@@ -218,27 +227,20 @@ class FlatExtractor:
"""
assert not self.buf or self.buf.endswith("\n")
begin_re = self.get_index_re(index)
while 1:
m = begin_re.search(self.buf)
if m:
self.buf = self.buf[m.start(2):]
return
self.buf = self.fileobj.read(self.blocksize)
self.buf += self.fileobj.readline()
if not self.buf:
self.at_end = 1
return
def get_index_re(self, index):
"""Return regular expression used to find index.
Override this in sub classes. The regular expression's second
group needs to start at the beginning of the record that
contains information about the object with the given index.
"""
assert 0, "Just a placeholder, must override this in subclasses"
while 1:
m = self.record_boundary_regexp.search(self.buf)
if not m: break
cur_index = self.filename_to_index(m.group(2))
if cur_index >= index:
self.buf = self.buf[m.start(1):]
return
else: self.buf = self.buf[m.end(1):]
def iterate_starting_with(self, index):
"""Iterate objects whose index starts with given index"""
......@@ -256,24 +258,24 @@ class FlatExtractor:
self.buf = self.buf[next_pos:]
assert not self.close()
def filename_to_index(self, filename):
"""Translate filename, possibly quoted, into an index tuple
The filename is the first group matched by
regexp_boundary_regexp.
"""
assert 0 # subclass
def close(self):
"""Return value of closing associated file"""
return self.fileobj.close()
class RorpExtractor(FlatExtractor):
"""Iterate rorps from metadata file"""
record_boundary_regexp = re.compile("\\nFile")
record_boundary_regexp = re.compile("(?:\\n|^)(File (.*?))\\n")
record_to_object = staticmethod(Record2RORP)
def get_index_re(self, index):
"""Find start of rorp record with given index"""
indexpath = index and '/'.join(index) or '.'
# Must double all backslashes, because they will be
# reinterpreted. For instance, to search for index \n
# (newline), it will be \\n (backslash n) in the file, so the
# regular expression is "File \\\\n\\n" (File two backslash n
# backslash n)
double_quote = re.sub("\\\\", "\\\\\\\\", indexpath)
return re.compile("(^|\\n)(File %s\\n)" % (double_quote,))
filename_to_index = staticmethod(quoted_filename_to_index)
class FlatFile:
......@@ -339,7 +341,7 @@ class FlatFile:
else: compressed = cls._rp.get_indexpath().endswith('.gz')
fileobj = cls._rp.open('rb', compress = compressed)
if restrict_index is None: return cls._extractor(fileobj).iterate()
if not restrict_index: return cls._extractor(fileobj).iterate()
else:
re = cls._extractor(fileobj)
return re.iterate_starting_with(restrict_index)
......
......@@ -1001,10 +1001,7 @@ class RPath(RORPath):
def get_acl(self):
"""Return access control list object, setting if necessary"""
try: acl = self.data['acl']
except KeyError:
acl = eas_acls.AccessControlList(self.index)
if not self.issym(): acl.read_from_rp(self)
self.data['acl'] = acl
except KeyError: acl = self.data['acl'] = acl_get(self)
return acl
def write_acl(self, acl):
......@@ -1015,14 +1012,7 @@ class RPath(RORPath):
def get_ea(self):
"""Return extended attributes object, setting if necessary"""
try: ea = self.data['ea']
except KeyError:
ea = eas_acls.ExtendedAttributes(self.index)
if not self.issym():
# Don't read from symlinks because they will be
# followed. Update this when llistxattr,
# etc. available
ea.read_from_rp(self)
self.data['ea'] = ea
except KeyError: ea = self.data['ea'] = ea_get(self)
return ea
def write_ea(self, ea):
......@@ -1068,4 +1058,9 @@ class RPathFileHook:
self.closing_thunk()
return result
import eas_acls # Put at end to avoid regress
# These two are overwritten by the eas_acls.py module. We can't
# import that module directory because of circular dependency
# problems.
def acl_get(rp): assert 0
def ea_get(rp): assert 0
......@@ -41,5 +41,13 @@ class CTest(unittest.TestCase):
"""Test running C.sync"""
C.sync()
def test_acl_quoting(self):
"""Test the acl_quote and acl_unquote functions"""
assert C.acl_quote('foo') == 'foo', C.acl_quote('foo')
assert C.acl_quote('\n') == '\\012', C.acl_quote('\n')
assert C.acl_unquote('\\012') == '\n'
s = '\\\n\t\145\n\01=='
assert C.acl_unquote(C.acl_quote(s)) == s
if __name__ == "__main__": unittest.main()
import unittest, os, time
import unittest, os, time, cStringIO
from commontest import *
from rdiff_backup.eas_acls import *
from rdiff_backup import Globals, rpath, Time
tempdir = rpath.RPath(Globals.local_connection, "testfiles/output")
restore_dir = rpath.RPath(Globals.local_connection,
"testfiles/restore_out")
class EATest(unittest.TestCase):
"""Test extended attributes"""
......@@ -27,6 +29,7 @@ class EATest(unittest.TestCase):
"""Make temp directory testfiles/output"""
if tempdir.lstat(): tempdir.delete()
tempdir.mkdir()
if restore_dir.lstat(): restore_dir.delete()
def testBasic(self):
"""Test basic writing and reading of extended attributes"""
......@@ -61,6 +64,41 @@ class EATest(unittest.TestCase):
(self.sample_ea.index, new_ea.index)
assert 0, "We shouldn't have gotten this far"
def testExtractor(self):
"""Test seeking inside a record list"""
record_list = """# file: 0foo
user.multiline=0sVGhpcyBpcyBhIGZhaXJseSBsb25nIGV4dGVuZGVkIGF0dHJpYnV0ZS4KCQkJIEVuY29kaW5nIGl0IHdpbGwgcmVxdWlyZSBzZXZlcmFsIGxpbmVzIG9mCgkJCSBiYXNlNjQusbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx
user.third=0saGVsbG8=
user.not_empty=0sZm9vYmFy
user.binary=0sAAECjC89Ig==
user.empty
# file: 1foo/bar/baz
user.multiline=0sVGhpcyBpcyBhIGZhaXJseSBsb25nIGV4dGVuZGVkIGF0dHJpYnV0ZS4KCQkJIEVuY29kaW5nIGl0IHdpbGwgcmVxdWlyZSBzZXZlcmFsIGxpbmVzIG9mCgkJCSBiYXNlNjQusbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx
user.third=0saGVsbG8=
user.binary=0sAAECjC89Ig==
user.empty
# file: 2foo/\\012
user.empty
"""
extractor = EAExtractor(cStringIO.StringIO(record_list))
ea_iter = extractor.iterate_starting_with(())
first = ea_iter.next()
assert first.index == ('0foo',), first
second = ea_iter.next()
assert second.index == ('1foo', 'bar', 'baz'), second
third = ea_iter.next() # Test quoted filenames
assert third.index == ('2foo', '\n'), third.index
try: ea_iter.next()
except StopIteration: pass
else: assert 0, "Too many elements in iterator"
extractor = EAExtractor(cStringIO.StringIO(record_list))
ea_iter = extractor.iterate_starting_with(('1foo', 'bar'))
assert ea_iter.next().index == ('1foo', 'bar', 'baz')
try: ea_iter.next()
except StopIteration: pass
else: assert 0, "Too many elements in iterator"
def make_backup_dirs(self):
"""Create testfiles/ea_test[12] directories
......@@ -137,6 +175,21 @@ class EATest(unittest.TestCase):
'testfiles/empty', 'testfiles/ea_test1']
BackupRestoreSeries(None, None, dirlist, compare_eas = 1)
def test_final_local(self):
"""Test backing up and restoring using 'rdiff-backup' script"""
self.make_backup_dirs()
self.make_temp()
rdiff_backup(1, 1, self.ea_testdir1.path, tempdir.path,
current_time = 10000)
assert CompareRecursive(self.ea_testdir1, tempdir, compare_eas = 1)
rdiff_backup(1, 1, self.ea_testdir2.path, tempdir.path,
current_time = 20000)
assert CompareRecursive(self.ea_testdir2, tempdir, compare_eas = 1)
rdiff_backup(1, 1, tempdir.path, restore_dir.path,
extra_options = '-r 10000')
assert CompareRecursive(self.ea_testdir1, restore_dir, compare_eas = 1)
class ACLTest(unittest.TestCase):
......@@ -181,6 +234,7 @@ other::---""")
"""Make temp directory testfile/output"""
if tempdir.lstat(): tempdir.delete()
tempdir.mkdir()
if restore_dir.lstat(): restore_dir.delete()
def testBasic(self):
"""Test basic writing and reading of ACLs"""
......@@ -227,6 +281,49 @@ other::---""")
assert new_acl2.eq_verbose(self.dir_acl)
assert 0
def testExtractor(self):
"""Test seeking inside a record list"""
record_list = """# file: 0foo
user::r--
user:ben:---
group::---
group:root:---
mask::---
other::---
# file: 1foo/bar/baz
user::r--
user:ben:---
group::---
group:root:---
mask::---
other::---
# file: 2foo/\\012
user::r--
user:ben:---
group::---
group:root:---
mask::---
other::---
"""
extractor = ACLExtractor(cStringIO.StringIO(record_list))
acl_iter = extractor.iterate_starting_with(())
first = acl_iter.next()
assert first.index == ('0foo',), first
second = acl_iter.next()
assert second.index == ('1foo', 'bar', 'baz'), second
third = acl_iter.next() # Test quoted filenames
assert third.index == ('2foo', '\n'), third.index
try: acl_iter.next()
except StopIteration: pass
else: assert 0, "Too many elements in iterator"
extractor = ACLExtractor(cStringIO.StringIO(record_list))
acl_iter = extractor.iterate_starting_with(('1foo', 'bar'))
assert acl_iter.next().index == ('1foo', 'bar', 'baz')
try: acl_iter.next()
except StopIteration: pass
else: assert 0, "Too many elements in iterator"
def make_backup_dirs(self):
"""Create testfiles/acl_test[12] directories"""
if self.acl_testdir1.lstat(): self.acl_testdir1.delete()
......@@ -295,6 +392,29 @@ other::---""")
'testfiles/empty', 'testfiles/acl_test1']
BackupRestoreSeries(None, None, dirlist, compare_acls = 1)
def test_final_local(self):
"""Test backing up and restoring using 'rdiff-backup' script"""
self.make_backup_dirs()
self.make_temp()
rdiff_backup(1, 1, self.acl_testdir1.path, tempdir.path,
current_time = 10000)
assert CompareRecursive(self.acl_testdir1, tempdir, compare_acls = 1)
rdiff_backup(1, 1, self.acl_testdir2.path, tempdir.path,
current_time = 20000)
assert CompareRecursive(self.acl_testdir2, tempdir, compare_acls = 1)
rdiff_backup(1, 1, tempdir.path, restore_dir.path,
extra_options = '-r 10000')
assert CompareRecursive(self.acl_testdir1, restore_dir,
compare_acls = 1)
restore_dir.delete()
rdiff_backup(1, 1, tempdir.path, restore_dir.path,
extra_options = '-r now')
assert CompareRecursive(self.acl_testdir2, restore_dir,
compare_acls = 1)
class CombinedTest(unittest.TestCase):
"""Test backing up and restoring directories with both EAs and ACLs"""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment