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
« 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 -*-
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:
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 )
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 ) )
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 ) )
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 )
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 ) )
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 )
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 )