1# Copyright (C) 2008, 2009, 2011 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Working tree content filtering support.
18
19A filter consists of a read converter, write converter pair.
20The content in the working tree is called the convenience format
21while the content actually stored is called the canonical format.
22The read converter produces canonical content from convenience
23content while the writer goes the other way.
24
25Converters have the following signatures::
26
27    read_converter(chunks) -> chunks
28    write_converter(chunks, context) -> chunks
29
30where:
31
32 * chunks is an iterator over a sequence of byte strings
33
34 * context is an optional ContentFilterContent object (possibly None)
35   providing converters access to interesting information, e.g. the
36   relative path of the file.
37
38Note that context is currently only supported for write converters.
39"""
40
41
42from io import (
43    BytesIO,
44    )
45
46from ..lazy_import import lazy_import
47lazy_import(globals(), """
48from breezy import (
49    config,
50    osutils,
51    registry,
52    )
53""")
54
55from .. import (
56    errors,
57    )
58
59
60class ContentFilter(object):
61
62    def __init__(self, reader, writer):
63        """Create a filter that converts content while reading and writing.
64
65        :param reader: function for converting convenience to canonical content
66        :param writer: function for converting canonical to convenience content
67        """
68        self.reader = reader
69        self.writer = writer
70
71    def __repr__(self):
72        return "reader: %s, writer: %s" % (self.reader, self.writer)
73
74
75class ContentFilterContext(object):
76    """Object providing information that filters can use."""
77
78    def __init__(self, relpath=None, tree=None):
79        """Create a context.
80
81        :param relpath: the relative path or None if this context doesn't
82           support that information.
83        :param tree: the Tree providing this file or None if this context
84           doesn't support that information.
85        """
86        self._relpath = relpath
87        self._tree = tree
88        # Cached values
89        self._revision_id = None
90        self._revision = None
91
92    def relpath(self):
93        """Relative path of file to tree-root."""
94        return self._relpath
95
96    def source_tree(self):
97        """Source Tree object."""
98        return self._tree
99
100    def revision_id(self):
101        """Id of revision that last changed this file."""
102        if self._revision_id is None:
103            if self._tree is not None:
104                self._revision_id = self._tree.get_file_revision(
105                    self._relpath)
106        return self._revision_id
107
108    def revision(self):
109        """Revision this variation of the file was introduced in."""
110        if self._revision is None:
111            rev_id = self.revision_id()
112            if rev_id is not None:
113                repo = getattr(self._tree, '_repository', None)
114                if repo is None:
115                    repo = self._tree.branch.repository
116                self._revision = repo.get_revision(rev_id)
117        return self._revision
118
119
120def filtered_input_file(f, filters):
121    """Get an input file that converts external to internal content.
122
123    :param f: the original input file
124    :param filters: the stack of filters to apply
125    :return: a file-like object, size
126    """
127    chunks = [f.read()]
128    for filter in filters:
129        if filter.reader is not None:
130            chunks = filter.reader(chunks)
131    text = b''.join(chunks)
132    return BytesIO(text), len(text)
133
134
135def filtered_output_bytes(chunks, filters, context=None):
136    """Convert byte chunks from internal to external format.
137
138    :param chunks: an iterator containing the original content
139    :param filters: the stack of filters to apply
140    :param context: a ContentFilterContext object passed to
141        each filter
142    :return: an iterator containing the content to output
143    """
144    if filters:
145        for filter in reversed(filters):
146            if filter.writer is not None:
147                chunks = filter.writer(chunks, context)
148    return chunks
149
150
151def internal_size_sha_file_byname(name, filters):
152    """Get size and sha of internal content given external content.
153
154    :param name: path to file
155    :param filters: the stack of filters to apply
156    """
157    with open(name, 'rb', 65000) as f:
158        if filters:
159            f, size = filtered_input_file(f, filters)
160        return osutils.size_sha_file(f)
161
162
163class FilteredStat(object):
164
165    def __init__(self, base, st_size=None):
166        self.st_mode = base.st_mode
167        self.st_size = st_size or base.st_size
168        self.st_mtime = base.st_mtime
169        self.st_ctime = base.st_ctime
170
171
172# The registry of filter stacks indexed by name.
173filter_stacks_registry = registry.Registry()
174
175
176# Cache of preferences -> stack
177# TODO: make this per branch (say) rather than global
178_stack_cache = {}
179
180
181def _get_registered_names():
182    """Get the list of names with filters registered."""
183    # Note: We may want to intelligently order these later.
184    # If so, the register_ fn will need to support an optional priority.
185    return filter_stacks_registry.keys()
186
187
188def _get_filter_stack_for(preferences):
189    """Get the filter stack given a sequence of preferences.
190
191    :param preferences: a sequence of (name,value) tuples where
192      name is the preference name and
193      value is the key into the filter stack map registered
194      for that preference.
195    """
196    if preferences is None:
197        return []
198    stack = _stack_cache.get(preferences)
199    if stack is not None:
200        return stack
201    stack = []
202    for k, v in preferences:
203        if v is None:
204            continue
205        try:
206            stack_map_lookup = filter_stacks_registry.get(k)
207        except KeyError:
208            # Some preferences may not have associated filters
209            continue
210        items = stack_map_lookup(v)
211        if items:
212            stack.extend(items)
213    _stack_cache[preferences] = stack
214    return stack
215
216
217def _reset_registry(value=None):
218    """Reset the filter stack registry.
219
220    This function is provided to aid testing. The expected usage is::
221
222      old = _reset_registry()
223      # run tests
224      _reset_registry(old)
225
226    :param value: the value to set the registry to or None for an empty one.
227    :return: the existing value before it reset.
228    """
229    global filter_stacks_registry
230    original = filter_stacks_registry
231    if value is None:
232        filter_stacks_registry = registry.Registry()
233    else:
234        filter_stacks_registry = value
235    _stack_cache.clear()
236    return original
237
238
239filter_stacks_registry.register_lazy('eol', 'breezy.filters.eol', 'eol_lookup')
240