1# pointer.py - Git-LFS pointer serialization
2#
3# Copyright 2017 Facebook, Inc.
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8from __future__ import absolute_import
9
10import re
11
12from mercurial.i18n import _
13
14from mercurial import (
15    error,
16    pycompat,
17)
18from mercurial.utils import stringutil
19
20
21class InvalidPointer(error.StorageError):
22    pass
23
24
25class gitlfspointer(dict):
26    VERSION = b'https://git-lfs.github.com/spec/v1'
27
28    def __init__(self, *args, **kwargs):
29        self[b'version'] = self.VERSION
30        super(gitlfspointer, self).__init__(*args)
31        self.update(pycompat.byteskwargs(kwargs))
32
33    @classmethod
34    def deserialize(cls, text):
35        try:
36            return cls(l.split(b' ', 1) for l in text.splitlines()).validate()
37        except ValueError:  # l.split returns 1 item instead of 2
38            raise InvalidPointer(
39                _(b'cannot parse git-lfs text: %s') % stringutil.pprint(text)
40            )
41
42    def serialize(self):
43        sortkeyfunc = lambda x: (x[0] != b'version', x)
44        items = sorted(pycompat.iteritems(self.validate()), key=sortkeyfunc)
45        return b''.join(b'%s %s\n' % (k, v) for k, v in items)
46
47    def oid(self):
48        return self[b'oid'].split(b':')[-1]
49
50    def size(self):
51        return int(self[b'size'])
52
53    # regular expressions used by _validate
54    # see https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md
55    _keyre = re.compile(br'\A[a-z0-9.-]+\Z')
56    _valuere = re.compile(br'\A[^\n]*\Z')
57    _requiredre = {
58        b'size': re.compile(br'\A[0-9]+\Z'),
59        b'oid': re.compile(br'\Asha256:[0-9a-f]{64}\Z'),
60        b'version': re.compile(br'\A%s\Z' % stringutil.reescape(VERSION)),
61    }
62
63    def validate(self):
64        """raise InvalidPointer on error. return self if there is no error"""
65        requiredcount = 0
66        for k, v in pycompat.iteritems(self):
67            if k in self._requiredre:
68                if not self._requiredre[k].match(v):
69                    raise InvalidPointer(
70                        _(b'unexpected lfs pointer value: %s=%s')
71                        % (k, stringutil.pprint(v))
72                    )
73                requiredcount += 1
74            elif not self._keyre.match(k):
75                raise InvalidPointer(_(b'unexpected lfs pointer key: %s') % k)
76            if not self._valuere.match(v):
77                raise InvalidPointer(
78                    _(b'unexpected lfs pointer value: %s=%s')
79                    % (k, stringutil.pprint(v))
80                )
81        if len(self._requiredre) != requiredcount:
82            miss = sorted(set(self._requiredre.keys()).difference(self.keys()))
83            raise InvalidPointer(
84                _(b'missing lfs pointer keys: %s') % b', '.join(miss)
85            )
86        return self
87
88
89deserialize = gitlfspointer.deserialize
90