Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/model_signing/_serialization/serialization.py: 49%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

37 statements  

1# Copyright 2024 The Sigstore Authors 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Machinery for serializing ML models.""" 

16 

17import abc 

18from collections.abc import Iterable 

19import errno 

20import logging 

21import pathlib 

22 

23from model_signing import manifest 

24 

25 

26logger = logging.getLogger(__name__) 

27 

28_WINDOWS_ERROR_CANT_RESOLVE_FILENAME = 1921 

29 

30 

31def check_file_or_directory( 

32 path: pathlib.Path, *, allow_symlinks: bool = False 

33) -> None: 

34 """Checks that the given path is either a file or a directory. 

35 

36 There is no support for sockets, pipes, or any other operating system 

37 concept abstracted as a file. 

38 

39 Furthermore, this would raise if the path is a broken symlink, if it doesn't 

40 exists or if there are permission errors. 

41 

42 Args: 

43 path: The path to check. 

44 allow_symlinks: Controls whether symbolic links are included. If a 

45 symlink is present but the flag is `False` (default) the 

46 serialization would raise an error. 

47 

48 Raises: 

49 ValueError: The path is neither a file or a directory, or the path 

50 is a symlink and `allow_symlinks` is false. 

51 """ 

52 if not allow_symlinks and path.is_symlink(): 

53 raise ValueError( 

54 f"Cannot use '{path}' because it is a symlink. This" 

55 " behavior can be changed with `allow_symlinks`." 

56 ) 

57 if not (path.is_file() or path.is_dir()): 

58 raise ValueError( 

59 f"Cannot use '{path}' as file or directory. It could be a" 

60 " special file, it could be missing, or there might be a" 

61 " permission issue." 

62 ) 

63 

64 

65def check_symlink(path: pathlib.Path, model_path: pathlib.Path) -> bool: 

66 """Warns about symlinks targeting outside the model root or forming cycles. 

67 

68 OMS v1.0 6.1.1: signer MUST report a warning when a symlink target is 

69 outside the model root, or when a symlink cycle is detected. 

70 

71 Args: 

72 path: The symlink path to check. 

73 model_path: The root of the model directory. 

74 

75 Returns: 

76 True if the symlink is usable, False if it should be skipped (cycle). 

77 

78 Raises: 

79 ValueError: The symlink cannot be resolved (broken symlink). 

80 """ 

81 try: 

82 target = path.resolve(strict=True) 

83 except RuntimeError: 

84 # Python 3.12 and earlier raise RuntimeError on symlink loops. 

85 logger.warning("Symlink cycle detected: '%s'", path) 

86 return False 

87 except OSError as err: 

88 if err.errno == errno.ELOOP or ( 

89 getattr(err, "winerror", None) 

90 == _WINDOWS_ERROR_CANT_RESOLVE_FILENAME 

91 ): 

92 logger.warning("Symlink cycle detected: '%s'", path) 

93 return False 

94 # A link that cannot be resolved is not a cycle. Skipping it would 

95 # leave it out of the manifest at signing and leave it unreported at 

96 # verification, so it must fail like any other unusable path. 

97 raise ValueError( 

98 f"Cannot use '{path}' because it is a broken symlink." 

99 ) from err 

100 

101 model_root = model_path.resolve() 

102 try: 

103 target.relative_to(model_root) 

104 except ValueError: 

105 logger.warning( 

106 "Symlink '%s' targets '%s' outside model root '%s'", 

107 path, 

108 target, 

109 model_root, 

110 ) 

111 

112 return True 

113 

114 

115def should_ignore( 

116 path: pathlib.Path, ignore_paths: Iterable[pathlib.Path] 

117) -> bool: 

118 """Determines if the provided path should be ignored during serialization. 

119 

120 Args: 

121 path: The path to check. 

122 ignore_paths: The paths to ignore while serializing a model. 

123 

124 Returns: 

125 Whether or not the provided path should be ignored. 

126 """ 

127 return any(path.is_relative_to(ignore_path) for ignore_path in ignore_paths) 

128 

129 

130class Serializer(metaclass=abc.ABCMeta): 

131 """Generic ML model format serializer.""" 

132 

133 @abc.abstractmethod 

134 def serialize( 

135 self, 

136 model_path: pathlib.Path, 

137 *, 

138 ignore_paths: Iterable[pathlib.Path] = frozenset(), 

139 ) -> manifest.Manifest: 

140 """Serializes the model given by the `model_path` argument. 

141 

142 Args: 

143 model_path: The path to the model. 

144 ignore_paths: The paths to ignore during serialization. If a 

145 provided path is a directory, all children of the directory are 

146 ignored. 

147 

148 Returns: 

149 The model's serialized manifest. 

150 """