1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf8 -*-
2#
3# Copyright 2002 Ben Escoto <ben@emerose.org>
4# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
5#
6# This file is part of duplicity.
7#
8# duplicity is free software; you can redistribute it and/or modify
9# under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# duplicity is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16# General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with duplicity; if not, write to the Free Software Foundation,
20# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22u"""Provides a high-level interface to some librsync functions
23
24This is a python wrapper around the lower-level _librsync module,
25which is written in C.  The goal was to use C as little as possible...
26
27"""
28
29from builtins import object
30from builtins import str
31
32import array
33import os
34import sys
35
36from . import _librsync
37
38if os.environ.get(u'READTHEDOCS') == u'True':
39    import mock  # pylint: disable=import-error
40    import duplicity
41    duplicity._librsync = mock.MagicMock()
42
43blocksize = _librsync.RS_JOB_BLOCKSIZE
44
45
46class librsyncError(Exception):
47    u"""Signifies error in internal librsync processing (bad signature, etc.)
48
49    underlying _librsync.librsyncError's are regenerated using this
50    class because the C-created exceptions are by default
51    unPickleable.  There is probably a way to fix this in _librsync,
52    but this scheme was easier.
53
54    """
55    pass
56
57
58class LikeFile(object):
59    u"""File-like object used by SigFile, DeltaFile, and PatchFile"""
60    mode = u"rb"
61
62    # This will be replaced in subclasses by an object with
63    # appropriate cycle() method
64    maker = None
65
66    def __init__(self, infile, need_seek=None):
67        u"""LikeFile initializer - zero buffers, set eofs off"""
68        self.check_file(infile, need_seek)
69        self.infile = infile
70        self.closed = self.infile_closed = None
71        self.inbuf = b""
72        self.outbuf = array.array(u'b')
73        self.eof = self.infile_eof = None
74
75    def check_file(self, file, need_seek=None):
76        u"""Raise type error if file doesn't have necessary attributes"""
77        if not hasattr(file, u"read"):
78            raise TypeError(u"Basis file must have a read() method")
79        if not hasattr(file, u"close"):
80            raise TypeError(u"Basis file must have a close() method")
81        if need_seek and not hasattr(file, u"seek"):
82            raise TypeError(u"Basis file must have a seek() method")
83
84    def read(self, length=-1):
85        u"""Build up self.outbuf, return first length bytes"""
86        if length == -1:
87            while not self.eof:
88                self._add_to_outbuf_once()
89            real_len = len(self.outbuf)
90        else:
91            while not self.eof and len(self.outbuf) < length:
92                self._add_to_outbuf_once()
93            real_len = min(length, len(self.outbuf))
94
95        if sys.version_info.major >= 3:
96            return_val = self.outbuf[:real_len].tobytes()
97        else:
98            return_val = self.outbuf[:real_len].tostring()
99        del self.outbuf[:real_len]
100        return return_val
101
102    def _add_to_outbuf_once(self):
103        u"""Add one cycle's worth of output to self.outbuf"""
104        if not self.infile_eof:
105            self._add_to_inbuf()
106        try:
107            self.eof, len_inbuf_read, cycle_out = self.maker.cycle(self.inbuf)
108        except _librsync.librsyncError as e:
109            raise librsyncError(str(e))
110        self.inbuf = self.inbuf[len_inbuf_read:]
111        if sys.version_info.major >= 3:
112            self.outbuf.frombytes(cycle_out)
113        else:
114            self.outbuf.fromstring(cycle_out)
115
116    def _add_to_inbuf(self):
117        u"""Make sure len(self.inbuf) >= blocksize"""
118        assert not self.infile_eof
119        while len(self.inbuf) < blocksize:
120            new_in = self.infile.read(blocksize)
121            if not new_in:
122                self.infile_eof = 1
123                assert not self.infile.close()
124                self.infile_closed = 1
125                break
126            self.inbuf += new_in
127
128    def close(self):
129        u"""Close infile"""
130        if not self.infile_closed:
131            assert not self.infile.close()
132        self.closed = 1
133
134
135class SigFile(LikeFile):
136    u"""File-like object which incrementally generates a librsync signature"""
137    def __init__(self, infile, blocksize=_librsync.RS_DEFAULT_BLOCK_LEN):
138        u"""SigFile initializer - takes basis file
139
140        basis file only needs to have read() and close() methods.  It
141        will be closed when we come to the end of the signature.
142
143        """
144        LikeFile.__init__(self, infile)
145        try:
146            self.maker = _librsync.new_sigmaker(blocksize)
147        except _librsync.librsyncError as e:
148            raise librsyncError(str(e))
149
150
151class DeltaFile(LikeFile):
152    u"""File-like object which incrementally generates a librsync delta"""
153    def __init__(self, signature, new_file):
154        u"""DeltaFile initializer - call with signature and new file
155
156        Signature can either be a string or a file with read() and
157        close() methods.  New_file also only needs to have read() and
158        close() methods.  It will be closed when self is closed.
159
160        """
161        LikeFile.__init__(self, new_file)
162        if isinstance(signature, bytes):
163            sig_string = signature
164        else:
165            self.check_file(signature)
166            sig_string = signature.read()
167            assert not signature.close()
168        try:
169            self.maker = _librsync.new_deltamaker(sig_string)
170        except _librsync.librsyncError as e:
171            raise librsyncError(str(e))
172
173
174class PatchedFile(LikeFile):
175    u"""File-like object which applies a librsync delta incrementally"""
176    def __init__(self, basis_file, delta_file):
177        u"""PatchedFile initializer - call with basis delta
178
179        Here basis_file must be a true Python file, because we may
180        need to seek() around in it a lot, and this is done in C.
181        delta_file only needs read() and close() methods.
182
183        """
184        LikeFile.__init__(self, delta_file)
185        try:
186            basis_file.fileno()
187        except:
188            u""" tempfile.TemporaryFile() only guarantees a true file
189            object on posix platforms. on cygwin/windows a file-like
190            object whose file attribute is the underlying true file
191            object is returned.
192            """
193            if hasattr(basis_file, u'file') and hasattr(basis_file.file, u'fileno'):
194                basis_file = basis_file.file
195            else:
196                raise TypeError(_(u"basis_file must be a (true) file or an object whose "
197                                  u"file attribute is the underlying true file object"))
198        try:
199            self.maker = _librsync.new_patchmaker(basis_file)
200        except _librsync.librsyncError as e:
201            raise librsyncError(str(e))
202
203
204class SigGenerator(object):
205    u"""Calculate signature.
206
207    Input and output is same as SigFile, but the interface is like md5
208    module, not filelike object
209
210    """
211    def __init__(self, blocksize=_librsync.RS_DEFAULT_BLOCK_LEN):
212        u"""Return new signature instance"""
213        try:
214            self.sig_maker = _librsync.new_sigmaker(blocksize)
215        except _librsync.librsyncError as e:
216            raise librsyncError(str(e))
217        self.gotsig = None
218        self.buffer = b""
219        self.sigstring_list = []
220
221    def update(self, buf):
222        u"""Add buf to data that signature will be calculated over"""
223        if self.gotsig:
224            raise librsyncError(u"SigGenerator already provided signature")
225        self.buffer += buf
226        while len(self.buffer) >= blocksize:
227            if self.process_buffer():
228                raise librsyncError(u"Premature EOF received from sig_maker")
229
230    def process_buffer(self):
231        u"""Run self.buffer through sig_maker, add to self.sig_string"""
232        try:
233            eof, len_buf_read, cycle_out = self.sig_maker.cycle(self.buffer)
234        except _librsync.librsyncError as e:
235            raise librsyncError(str(e))
236        self.buffer = self.buffer[len_buf_read:]
237        self.sigstring_list.append(cycle_out)
238        return eof
239
240    def getsig(self):
241        u"""Return signature over given data"""
242        while not self.process_buffer():
243            pass  # keep running until eof
244        return b''.join(self.sigstring_list)
245