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