Coverage for sources/mimeogram/fsprotect/cache.py: 87%
97 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-22 03:00 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-22 03:00 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
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#============================================================================#
21''' Cache for filesystem protection checks. '''
24from __future__ import annotations
26from . import __
27from . import core as _core
30_scribe = __.produce_scribe( __name__ )
33class Rule(
34 metaclass = __.ImmutableStandardDataclass,
35 decorators = ( __.standard_dataclass, ),
36):
37 ''' Rule for path protection. '''
39 paths: frozenset[ __.Path ]
40 patterns: frozenset[ str ] = frozenset( )
43class Cache( _core.Protector, decorators = ( __.standard_dataclass, ) ):
44 ''' Cache of protected paths and patterns for platform. '''
46 rules: dict[ _core.Reasons, Rule ]
47 defaults_disablement: frozenset[ str ]
48 rules_supercession: __.ImmutableDictionary[
49 __.Path, tuple[ frozenset[ str ], frozenset[ str ] ] ]
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 )
65 def verify( self, path: __.Path ) -> _core.Status: # pylint: disable=too-many-locals
66 ''' Verifies if a path should be protected using cached data. '''
67 path = _normalize_path( path )
68 _scribe.debug( f"Path: {path}" )
70 if any( part in self.defaults_disablement for part in path.parts ):
71 return _core.Status( path = path, active = False )
73 for dir_path, ( ignore, protect ) in self.rules_supercession.items( ):
74 dir_path_ = _normalize_path( dir_path )
75 if not path.is_relative_to( dir_path_ ): continue
76 rel_path = path.relative_to( dir_path_ )
77 if _check_path_patterns( rel_path, ignore ):
78 return _core.Status( path = path, active = False )
79 if _check_path_patterns( rel_path, protect ):
80 return _core.Status(
81 path = path,
82 reason = _core.Reasons.PlatformSensitive,
83 active = True )
85 for reason, rule in self.rules.items( ):
86 for protected_path in rule.paths:
87 protected_path_ = _normalize_path( protected_path )
88 if path.is_relative_to( protected_path_ ):
89 return _core.Status(
90 path = path,
91 reason = reason,
92 active = True )
93 if _check_path_patterns( path, rule.patterns ):
94 return _core.Status(
95 path = path,
96 reason = reason,
97 active = True )
99 return _core.Status( path = path, active = False )
102def provide_credentials_locations(
103 rules: dict[ _core.Reasons, Rule ]
104) -> None:
105 ''' Provides common locations for credentials and other secrets. '''
106 from . import home as module
107 home = __.Path.home( )
108 cred_paths = {
109 home / path for path in module.discover_sensitive_locations( ) }
110 rules[ _core.Reasons.Credentials ] = Rule(
111 paths = frozenset( cred_paths ) )
114def provide_project_locations(
115 rules: dict[ _core.Reasons, Rule ]
116) -> None:
117 ''' Provides IDE and VCS locations relative to project. '''
118 from . import project as module
119 project_sensitive = module.discover_sensitive_locations( )
120 # TODO: Consider whether these patterns are compatible with Windows.
121 rules[ _core.Reasons.Concealment ] = Rule(
122 paths = frozenset( ),
123 patterns = frozenset(
124 f"**/{path}/**" for path in project_sensitive ) )
127def _check_path_patterns( path: __.Path, patterns: frozenset[ str ] ) -> bool:
128 ''' Checks if path matches any of the glob patterns. '''
129 from wcmatch import glob
130 str_path = str( path )
131 return glob.globmatch( str_path, list( patterns ), flags = glob.GLOBSTAR )
134def discover_platform_locations(
135 auxdata: __.Globals, rules: dict[ _core.Reasons, Rule ]
136) -> None:
137 ''' Discovers system and user locations based on platform. '''
138 match __.sys.platform:
139 case 'darwin': 139 ↛ 140line 139 didn't jump to line 140 because the pattern on line 139 never matched
140 from . import macos as module
141 sys_paths = module.discover_system_paths( )
142 user_paths = module.discover_user_paths( )
143 case 'win32': 143 ↛ 144line 143 didn't jump to line 144 because the pattern on line 143 never matched
144 from . import windows as module
145 sys_paths = module.discover_system_paths( )
146 user_paths = module.discover_user_paths( )
147 case _:
148 from . import unix as module
149 sys_paths = module.discover_system_paths( )
150 user_paths: frozenset[ __.Path ] = frozenset( )
151 rules[ _core.Reasons.OsDirectory ] = Rule( paths = frozenset( sys_paths ) )
152 config_paths = (
153 user_paths | { __.Path( auxdata.directories.user_config_path ), } )
154 rules[ _core.Reasons.UserConfiguration ] = Rule(
155 paths = frozenset( config_paths ) )
158def _expand_location( path: str ) -> __.Path:
159 ''' Expands path with home directory and environment variables. '''
160 expanded = __.os.path.expanduser( __.os.path.expandvars( path ) )
161 if ( __.sys.platform == 'win32' # pylint: disable=magic-value-comparison 161 ↛ 165line 161 didn't jump to line 165 because the condition on line 161 was never true
162 and expanded.startswith( '/' )
163 and not expanded.startswith( '//' ) # Skip UNC paths
164 ):
165 expanded = expanded.lstrip( '/' )
166 path_obj = __.Path( expanded )
167 if not path_obj.is_absolute( ):
168 path_obj = __.Path( __.Path.cwd( ).drive + '/' + expanded )
169 else: path_obj = __.Path( expanded )
170 return _normalize_path( path_obj.resolve( ) )
173def _normalize_path( path: __.Path ) -> __.Path:
174 ''' Normalizes path for consistent comparison across platforms. '''
175 resolved = path.resolve( )
176 if __.sys.platform == 'win32' and resolved.drive: # pylint: disable=magic-value-comparison 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 return __.Path(
178 resolved.drive.lower( )
179 + str( resolved )[ len( resolved.drive ): ] )
180 return resolved
183def _process_configuration(
184 auxdata: __.Globals,
185 rules: dict[ _core.Reasons, Rule ],
186) -> tuple[
187 frozenset[ str ],
188 __.ImmutableDictionary[
189 __.Path, tuple[ frozenset[ str ], frozenset[ str ] ] ],
190]:
191 config = auxdata.configuration.get( 'protection', { } )
192 if not config: return frozenset( ), __.ImmutableDictionary( )
193 # Additional locations and patterns.
194 locations_add = {
195 _expand_location( path )
196 for path in config.get( 'additional-locations', ( ) ) }
197 patterns_add = set( config.get( 'additional-patterns', ( ) ) )
198 rules[ _core.Reasons.CustomAddition ] = Rule(
199 paths = frozenset( locations_add ),
200 patterns = frozenset( patterns_add ) )
201 # Override defaults.
202 patterns_remove = frozenset( config.get( 'defaults-disablement', ( ) ) )
203 # Process directory overrides
204 supercession_rules: dict[
205 __.Path, tuple[ frozenset[ str ], frozenset[ str ] ] ] = { }
206 for dir_path, dir_rules in config.get(
207 'rules-supercession', { }
208 ).items( ):
209 full_path = _expand_location( dir_path )
210 supercession_rules[ full_path ] = (
211 frozenset( dir_rules.get( 'ignore', [ ] ) ),
212 frozenset( dir_rules.get( 'protect', [ ] ) ) )
213 return patterns_remove, __.ImmutableDictionary( supercession_rules )