Coverage for sources/mimeogram/fsprotect/cache.py: 93%

84 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-16 02:11 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

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

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

8# You may obtain a copy of the License at # 

9# # 

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

11# # 

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

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

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

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

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Cache for filesystem protection checks. ''' 

22 

23 

24from __future__ import annotations 

25 

26from . import __ 

27from . import core as _core 

28 

29 

30_scribe = __.produce_scribe( __name__ ) 

31 

32 

33class Rule( 

34 metaclass = __.ImmutableStandardDataclass, 

35 decorators = ( __.standard_dataclass, ), 

36): 

37 ''' Rule for path protection. ''' 

38 

39 paths: frozenset[ __.Path ] 

40 patterns: frozenset[ str ] = frozenset( ) 

41 

42 

43class Cache( _core.Protector, decorators = ( __.standard_dataclass, ) ): 

44 ''' Cache of protected paths and patterns for platform. ''' 

45 

46 rules: dict[ _core.Reasons, Rule ] 

47 defaults_disablement: frozenset[ str ] 

48 rules_supercession: __.ImmutableDictionary[ 

49 __.Path, tuple[ frozenset[ str ], frozenset[ str ] ] ] 

50 

51 @classmethod 

52 def from_configuration( selfclass, auxdata: __.Globals ) -> Cache: 

53 ''' Initializes protection cache for current platform. ''' 

54 _scribe.debug( 'Initializing protection cache.' ) 

55 rules: dict[ _core.Reasons, Rule ] = { } 

56 discover_platform_locations( auxdata, rules ) 

57 provide_credentials_locations( rules ) 

58 provide_project_locations( rules ) 

59 disables, supercedes = _process_configuration( auxdata, rules ) 

60 return selfclass( 

61 rules = rules, 

62 defaults_disablement = disables, 

63 rules_supercession = supercedes ) 

64 

65 def verify( self, path: __.Path ) -> _core.Status: 

66 ''' Verifies if a path should be protected using cached data. ''' 

67 path = path.resolve( ) 

68 _scribe.debug( f"Path: {path}" ) 

69 if any( part in self.defaults_disablement for part in path.parts ): 

70 return _core.Status( path = path, active = False ) 

71 for dir_path, ( ignore, protect ) in self.rules_supercession.items( ): 

72 if not path.is_relative_to( dir_path ): continue 

73 rel_path = path.relative_to( dir_path ) 

74 if _check_path_patterns( rel_path, ignore ): 

75 return _core.Status( path = path, active = False ) 

76 if _check_path_patterns( rel_path, protect ): 

77 return _core.Status( 

78 path = path, 

79 reason = _core.Reasons.PlatformSensitive, 

80 active = True ) 

81 for reason, rule in self.rules.items( ): 

82 for protected_path in rule.paths: 

83 if path.is_relative_to( protected_path ): 

84 return _core.Status( 

85 path = path, 

86 reason = reason, 

87 active = True ) 

88 if _check_path_patterns( path, rule.patterns ): 

89 return _core.Status( 

90 path = path, 

91 reason = reason, 

92 active = True ) 

93 return _core.Status( path = path, active = False ) 

94 

95 

96def provide_credentials_locations( 

97 rules: dict[ _core.Reasons, Rule ] 

98) -> None: 

99 ''' Provides common locations for credentials and other secrets. ''' 

100 from . import home as module 

101 home = __.Path.home( ) 

102 cred_paths = { 

103 home / path for path in module.discover_sensitive_locations( ) } 

104 rules[ _core.Reasons.Credentials ] = Rule( 

105 paths = frozenset( cred_paths ) ) 

106 

107 

108def provide_project_locations( 

109 rules: dict[ _core.Reasons, Rule ] 

110) -> None: 

111 ''' Provides IDE and VCS locations relative to project. ''' 

112 from . import project as module 

113 project_sensitive = module.discover_sensitive_locations( ) 

114 # TODO: Consider whether these patterns are compatible with Windows. 

115 rules[ _core.Reasons.Concealment ] = Rule( 

116 paths = frozenset( ), 

117 patterns = frozenset( 

118 f"**/{path}/**" for path in project_sensitive ) ) 

119 

120 

121def _check_path_patterns( path: __.Path, patterns: frozenset[ str ] ) -> bool: 

122 ''' Checks if path matches any of the glob patterns. ''' 

123 from wcmatch import glob 

124 str_path = str( path ) 

125 return glob.globmatch( str_path, list( patterns ), flags = glob.GLOBSTAR ) 

126 

127 

128def discover_platform_locations( 

129 auxdata: __.Globals, rules: dict[ _core.Reasons, Rule ] 

130) -> None: 

131 ''' Discovers system and user locations based on platform. ''' 

132 match __.sys.platform: 

133 case 'darwin': 133 ↛ 134line 133 didn't jump to line 134 because the pattern on line 133 never matched

134 from . import macos as module 

135 sys_paths = module.discover_system_paths( ) 

136 user_paths = module.discover_user_paths( ) 

137 case 'win32': 137 ↛ 138line 137 didn't jump to line 138 because the pattern on line 137 never matched

138 from . import windows as module 

139 sys_paths = module.discover_system_paths( ) 

140 user_paths = module.discover_user_paths( ) 

141 case _: 

142 from . import unix as module 

143 sys_paths = module.discover_system_paths( ) 

144 user_paths: frozenset[ __.Path ] = frozenset( ) 

145 rules[ _core.Reasons.OsDirectory ] = Rule( paths = frozenset( sys_paths ) ) 

146 config_paths = ( 

147 user_paths | { __.Path( auxdata.directories.user_config_path ), } ) 

148 rules[ _core.Reasons.UserConfiguration ] = Rule( 

149 paths = frozenset( config_paths ) ) 

150 

151 

152def _expand_location( path: str ) -> __.Path: 

153 ''' Expands path with home directory and environment variables. ''' 

154 expanded = __.os.path.expanduser( __.os.path.expandvars( path ) ) 

155 return __.Path( expanded ) 

156 

157 

158def _process_configuration( 

159 auxdata: __.Globals, 

160 rules: dict[ _core.Reasons, Rule ], 

161) -> tuple[ 

162 frozenset[ str ], 

163 __.ImmutableDictionary[ 

164 __.Path, tuple[ frozenset[ str ], frozenset[ str ] ] ], 

165]: 

166 config = auxdata.configuration.get( 'protection', { } ) 

167 if not config: return frozenset( ), __.ImmutableDictionary( ) 

168 # Additional locations and patterns. 

169 locations_add = { 

170 _expand_location( path ) 

171 for path in config.get( 'additional-locations', ( ) ) } 

172 patterns_add = set( config.get( 'additional-patterns', ( ) ) ) 

173 rules[ _core.Reasons.CustomAddition ] = Rule( 

174 paths = frozenset( locations_add ), 

175 patterns = frozenset( patterns_add ) ) 

176 # Override defaults. 

177 patterns_remove = frozenset( config.get( 'defaults-disablement', ( ) ) ) 

178 # Process directory overrides 

179 supercession_rules: dict[ 

180 __.Path, tuple[ frozenset[ str ], frozenset[ str ] ] ] = { } 

181 for dir_path, dir_rules in config.get( 

182 'rules-supercession', { } 

183 ).items( ): 

184 full_path = _expand_location( dir_path ) 

185 supercession_rules[ full_path ] = ( 

186 frozenset( dir_rules.get( 'ignore', [ ] ) ), 

187 frozenset( dir_rules.get( 'protect', [ ] ) ) ) 

188 return patterns_remove, __.ImmutableDictionary( supercession_rules )