tenseleyflow/sultree / 99fc848

Browse files

I forget

Authored by espadonne
SHA
99fc848819cd7f81efeb4a52ba8507c766d264c5
Parents
15f21e0
Tree
00b83ee

6 changed files

StatusFile+-
M Makefile 9 1
A src/sultree/arg_definitions.py 209 0
A src/sultree/constants.py 106 0
A src/sultree/tree_renderer.py 333 0
A src/sultree/validators.py 228 0
M sultree.spec 5 3
Makefilemodified
@@ -5,12 +5,13 @@ PYTHON = python3
5
 PACKAGE = sultree
5
 PACKAGE = sultree
6
 VERSION = 0.0.8
6
 VERSION = 0.0.8
7
 
7
 
8
-.PHONY: help build install test clean dist rpm dev-install check format
8
+.PHONY: help build install uninstall test clean dist rpm dev-install check format
9
 
9
 
10
 help:
10
 help:
11
 	@echo "Available targets:"
11
 	@echo "Available targets:"
12
 	@echo "  build       - Build the package"
12
 	@echo "  build       - Build the package"
13
 	@echo "  install     - Install the package"
13
 	@echo "  install     - Install the package"
14
+	@echo "  uninstall   - Uninstall the package"
14
 	@echo "  dev-install - Install in development mode"
15
 	@echo "  dev-install - Install in development mode"
15
 	@echo "  test        - Run tests"
16
 	@echo "  test        - Run tests"
16
 	@echo "  check       - Run code quality checks"
17
 	@echo "  check       - Run code quality checks"
@@ -25,6 +26,13 @@ build:
25
 install:
26
 install:
26
 	$(PYTHON) -m pip install .
27
 	$(PYTHON) -m pip install .
27
 
28
 
29
+uninstall:
30
+	@echo "Uninstalling $(PACKAGE)..."
31
+	$(PYTHON) -m pip uninstall $(PACKAGE) -y || echo "Package not installed via pip"
32
+	@echo "Removing any remaining executables..."
33
+	@rm -f ~/.local/bin/$(PACKAGE) 2>/dev/null || true
34
+	@echo "Uninstall complete!"
35
+
28
 dev-install:
36
 dev-install:
29
 	$(PYTHON) -m pip install -e .[dev]
37
 	$(PYTHON) -m pip install -e .[dev]
30
 
38
 
src/sultree/arg_definitions.pyadded
@@ -0,0 +1,209 @@
1
+"""
2
+Data-driven argument definitions for sultree.
3
+
4
+This module contains all command-line argument definitions in a clean,
5
+table-driven format to eliminate repetitive argparse code.
6
+"""
7
+
8
+from dataclasses import dataclass
9
+from typing import List, Optional, Callable, Any, Dict
10
+
11
+from .validators import (
12
+    validate_depth,
13
+    validate_file_limit, 
14
+    validate_pattern,
15
+    validate_selinux_pattern,
16
+    validate_path
17
+)
18
+
19
+
20
+@dataclass
21
+class ArgumentDefinition:
22
+    """Defines a command-line argument."""
23
+    short_name: Optional[str] = None
24
+    long_name: str = ''
25
+    action: str = 'store'
26
+    type_func: Optional[Callable] = None
27
+    dest: Optional[str] = None
28
+    metavar: Optional[str] = None
29
+    help_text: str = ''
30
+    default: Any = None
31
+    nargs: Optional[str] = None
32
+
33
+
34
+# Data-driven argument definitions table
35
+ARGUMENT_DEFINITIONS = [
36
+    # Positional arguments
37
+    ArgumentDefinition(
38
+        long_name='directories',
39
+        action='store',
40
+        nargs='*',
41
+        type_func=validate_path,
42
+        help_text='directories to tree (default: current directory)',
43
+        default=None  # Will be set to [Path('.')] in parsing
44
+    ),
45
+    
46
+    # Basic listing options  
47
+    ArgumentDefinition(
48
+        short_name='-a',
49
+        long_name='--all',
50
+        action='store_true',
51
+        help_text='show all files (including hidden files)'
52
+    ),
53
+    ArgumentDefinition(
54
+        short_name='-d',
55
+        long_name='--dirs-only',
56
+        action='store_true',
57
+        help_text='list directories only'
58
+    ),
59
+    ArgumentDefinition(
60
+        short_name='-l',
61
+        long_name='--follow-links',
62
+        action='store_true',
63
+        help_text='follow symbolic links like directories'
64
+    ),
65
+    ArgumentDefinition(
66
+        short_name='-f',
67
+        long_name='--full-path',
68
+        action='store_true',
69
+        help_text='print full path prefix for each file'
70
+    ),
71
+    ArgumentDefinition(
72
+        short_name='-x',
73
+        long_name='--one-file-system',
74
+        action='store_true',
75
+        help_text='stay on current filesystem only'
76
+    ),
77
+    
78
+    # Depth control
79
+    ArgumentDefinition(
80
+        short_name='-L',
81
+        long_name='--level',
82
+        type_func=validate_depth,
83
+        metavar='level',
84
+        help_text='descend only level directories deep'
85
+    ),
86
+    
87
+    # Pattern filtering
88
+    ArgumentDefinition(
89
+        short_name='-P',
90
+        long_name='--pattern',
91
+        type_func=validate_pattern,
92
+        action='append',
93
+        dest='include_patterns',
94
+        metavar='pattern',
95
+        help_text='list only files that match the pattern'
96
+    ),
97
+    ArgumentDefinition(
98
+        short_name='-I',
99
+        long_name='--ignore',
100
+        type_func=validate_pattern,
101
+        action='append',
102
+        dest='exclude_patterns',
103
+        metavar='pattern',
104
+        help_text='do not list files that match the pattern'
105
+    ),
106
+    ArgumentDefinition(
107
+        long_name='--match-dirs',
108
+        action='store_true',
109
+        help_text='include directory names in pattern matching'
110
+    ),
111
+    ArgumentDefinition(
112
+        long_name='--ignore-case',
113
+        action='store_true',
114
+        help_text='ignore case when pattern matching'
115
+    ),
116
+    
117
+    # File limits
118
+    ArgumentDefinition(
119
+        long_name='--filelimit',
120
+        type_func=validate_file_limit,
121
+        metavar='N',
122
+        help_text='do not descend dirs with more than N files'
123
+    ),
124
+    
125
+    # SELinux options
126
+    ArgumentDefinition(
127
+        short_name='-S',
128
+        long_name='--selinux',
129
+        type_func=validate_selinux_pattern,
130
+        action='append',
131
+        dest='selinux_patterns',
132
+        metavar='pattern',
133
+        help_text='show only files matching SELinux pattern (can be used multiple times)'
134
+    ),
135
+    
136
+    # Output options
137
+    ArgumentDefinition(
138
+        long_name='--no-report',
139
+        action='store_true',
140
+        help_text='turn off file/directory count at end of tree listing'
141
+    ),
142
+    ArgumentDefinition(
143
+        long_name='--version',
144
+        action='version',
145
+        help_text='show version information',
146
+        # Note: version string will be set dynamically
147
+    ),
148
+]
149
+
150
+
151
+def build_argument_kwargs(arg_def: ArgumentDefinition) -> Dict[str, Any]:
152
+    """
153
+    Convert ArgumentDefinition to kwargs for argparse.add_argument().
154
+    
155
+    Args:
156
+        arg_def: Argument definition
157
+        
158
+    Returns:
159
+        Dictionary of kwargs for add_argument()
160
+    """
161
+    kwargs = {}
162
+    
163
+    # Action
164
+    if arg_def.action:
165
+        kwargs['action'] = arg_def.action
166
+        
167
+    # Type function
168
+    if arg_def.type_func:
169
+        kwargs['type'] = arg_def.type_func
170
+        
171
+    # Destination
172
+    if arg_def.dest:
173
+        kwargs['dest'] = arg_def.dest
174
+        
175
+    # Metavar
176
+    if arg_def.metavar:
177
+        kwargs['metavar'] = arg_def.metavar
178
+        
179
+    # Help text
180
+    if arg_def.help_text:
181
+        kwargs['help'] = arg_def.help_text
182
+        
183
+    # Default value
184
+    if arg_def.default is not None:
185
+        kwargs['default'] = arg_def.default
186
+        
187
+    # Nargs
188
+    if arg_def.nargs:
189
+        kwargs['nargs'] = arg_def.nargs
190
+    
191
+    return kwargs
192
+
193
+
194
+def get_argument_names(arg_def: ArgumentDefinition) -> List[str]:
195
+    """
196
+    Get the argument names (short and long) for an ArgumentDefinition.
197
+    
198
+    Args:
199
+        arg_def: Argument definition
200
+        
201
+    Returns:
202
+        List of argument names
203
+    """
204
+    names = []
205
+    if arg_def.short_name:
206
+        names.append(arg_def.short_name)
207
+    if arg_def.long_name:
208
+        names.append(arg_def.long_name)
209
+    return names
src/sultree/constants.pyadded
@@ -0,0 +1,106 @@
1
+"""
2
+Constants and configuration values for sultree.
3
+
4
+This module centralizes all magic numbers and configuration values,
5
+making them data-driven and easier to maintain.
6
+"""
7
+
8
+from typing import Dict, Any
9
+import dataclasses
10
+
11
+
12
+@dataclasses.dataclass(frozen=True)
13
+class Limits:
14
+    """Resource and validation limits."""
15
+    
16
+    # Input validation limits
17
+    MAX_DEPTH: int = 1000
18
+    MAX_FILE_LIMIT: int = 100000
19
+    MAX_PATTERN_LENGTH: int = 1000
20
+    MAX_SELINUX_PATTERN_LENGTH: int = 500
21
+    MAX_PATH_LENGTH: int = 4096
22
+    
23
+    # Performance tuning
24
+    SELINUX_QUERY_TIMEOUT: int = 5
25
+    MAX_LINE_LENGTH: int = 2000
26
+    MAX_FILE_READ_LINES: int = 2000
27
+    
28
+    # SELinux context fields (allows up to 6 for complex MLS levels)
29
+    MAX_SELINUX_FIELDS: int = 6
30
+
31
+
32
+@dataclasses.dataclass(frozen=True)
33
+class TreeChars:
34
+    """Tree drawing characters for different terminal types."""
35
+    
36
+    # Unicode tree characters (default)
37
+    UNICODE: Dict[str, str] = dataclasses.field(default_factory=lambda: {
38
+        'vertical': '│',
39
+        'branch': '├──',
40
+        'last': '└──',
41
+        'space': '    ',
42
+        'continuation': '│   '
43
+    })
44
+    
45
+    # ASCII fallback characters
46
+    ASCII: Dict[str, str] = dataclasses.field(default_factory=lambda: {
47
+        'vertical': '|',
48
+        'branch': '|--',
49
+        'last': '`--',
50
+        'space': '    ',
51
+        'continuation': '|   '
52
+    })
53
+
54
+
55
+@dataclasses.dataclass(frozen=True)
56
+class Security:
57
+    """Security-related constants."""
58
+    
59
+    # Dangerous characters to remove from patterns
60
+    DANGEROUS_CHARS: tuple = (';', '&', '|', '`', '$', '(', ')', '<', '>', '\n', '\r')
61
+    
62
+    # Dangerous regex patterns
63
+    DANGEROUS_REGEX_PATTERNS: tuple = (
64
+        r'\(\?\(\?\!',  # Catastrophic backtracking
65
+        r'\(\?\(\?\=',  # Catastrophic backtracking
66
+        r'\*\*\*+',     # Excessive repetition
67
+    )
68
+    
69
+    # SELinux extended attribute name
70
+    SELINUX_XATTR: str = 'security.selinux'
71
+
72
+
73
+@dataclasses.dataclass(frozen=True)
74
+class ExitCodes:
75
+    """Standard exit codes."""
76
+    
77
+    SUCCESS: int = 0
78
+    GENERAL_ERROR: int = 1
79
+    INVALID_ARGS: int = 2
80
+    PERMISSION_ERROR: int = 3
81
+    PARTIAL_SUCCESS: int = 4  # Some directories processed successfully
82
+    INTERRUPTED: int = 130  # Ctrl+C
83
+
84
+
85
+@dataclasses.dataclass(frozen=True)
86
+class Config:
87
+    """Main configuration object combining all constants."""
88
+    
89
+    limits: Limits = dataclasses.field(default_factory=Limits)
90
+    tree_chars: TreeChars = dataclasses.field(default_factory=TreeChars)
91
+    security: Security = dataclasses.field(default_factory=Security)
92
+    exit_codes: ExitCodes = dataclasses.field(default_factory=ExitCodes)
93
+
94
+
95
+# Global configuration instance
96
+CONFIG = Config()
97
+
98
+
99
+def get_config() -> Config:
100
+    """
101
+    Get the global configuration instance.
102
+    
103
+    Returns:
104
+        Configuration object with all constants
105
+    """
106
+    return CONFIG
src/sultree/tree_renderer.pyadded
@@ -0,0 +1,333 @@
1
+"""
2
+Proper tree rendering module for sultree.
3
+
4
+This module implements the visual tree structure that makes tree commands useful,
5
+with proper indentation, connectors, and hierarchical display.
6
+"""
7
+
8
+import sys
9
+from pathlib import Path
10
+from typing import List, Dict, Optional, TextIO, Tuple
11
+from dataclasses import dataclass
12
+
13
+from .constants import CONFIG
14
+from .traversal import FileEntry
15
+from .selinux import get_selinux_context
16
+
17
+
18
+@dataclass
19
+class TreeNode:
20
+    """Represents a node in the tree structure."""
21
+    entry: FileEntry
22
+    children: List['TreeNode']
23
+    is_last: bool = False  # Is this the last child of its parent?
24
+    
25
+    def __post_init__(self):
26
+        if not hasattr(self, 'children') or self.children is None:
27
+            self.children = []
28
+
29
+
30
+class TreeRenderer:
31
+    """
32
+    Renders directory trees with proper visual structure.
33
+    
34
+    Handles the tree drawing characters, indentation, and hierarchical display
35
+    that makes tree commands visually useful.
36
+    """
37
+    
38
+    def __init__(self, show_full_path: bool = False, 
39
+                 show_selinux: bool = False,
40
+                 use_ascii: bool = False,
41
+                 output_file: Optional[TextIO] = None):
42
+        """
43
+        Initialize tree renderer.
44
+        
45
+        Args:
46
+            show_full_path: Show full paths instead of just names
47
+            show_selinux: Include SELinux contexts in output
48
+            use_ascii: Use ASCII characters instead of Unicode
49
+            output_file: Output file (default: stdout)
50
+        """
51
+        self.show_full_path = show_full_path
52
+        self.show_selinux = show_selinux
53
+        self.output_file = output_file or sys.stdout
54
+        
55
+        # Choose character set based on terminal capabilities
56
+        if use_ascii or not self._supports_unicode():
57
+            self.chars = CONFIG.tree_chars.ASCII
58
+        else:
59
+            self.chars = CONFIG.tree_chars.UNICODE
60
+            
61
+        # Statistics tracking
62
+        self.dir_count = 0
63
+        self.file_count = 0
64
+        
65
+    def _supports_unicode(self) -> bool:
66
+        """
67
+        Check if terminal supports Unicode characters.
68
+        
69
+        Returns:
70
+            True if Unicode is supported
71
+        """
72
+        try:
73
+            # Check encoding
74
+            encoding = self.output_file.encoding or 'ascii'
75
+            if 'utf' not in encoding.lower():
76
+                return False
77
+                
78
+            # Check if we can write Unicode characters
79
+            self.output_file.write('')  # Test write access
80
+            return True
81
+            
82
+        except (AttributeError, UnicodeError):
83
+            return False
84
+    
85
+    def render_tree(self, root_path: Path, root_node: TreeNode, show_report: bool = True) -> None:
86
+        """
87
+        Render the complete tree structure.
88
+        
89
+        Args:
90
+            root_path: Root directory path
91
+            root_node: Root tree node
92
+            show_report: Whether to show file/directory count at end
93
+        """
94
+        # Print root directory
95
+        self._print_root(root_path)
96
+        
97
+        # Render tree structure
98
+        self._render_node_children(root_node.children, prefix="", is_root=True)
99
+        
100
+        # Print summary report
101
+        if show_report:
102
+            self._print_report()
103
+    
104
+    def _render_node_children(self, nodes: List[TreeNode], prefix: str, is_root: bool = False) -> None:
105
+        """
106
+        Render a list of child nodes with proper tree structure.
107
+        
108
+        Args:
109
+            nodes: List of child nodes to render
110
+            prefix: Current prefix for indentation
111
+            is_root: Whether these are direct children of root
112
+        """
113
+        for i, node in enumerate(nodes):
114
+            is_last = (i == len(nodes) - 1)
115
+            self._render_node(node, prefix, is_last, is_root)
116
+    
117
+    def _render_node(self, node: TreeNode, prefix: str, is_last: bool, is_root: bool = False) -> None:
118
+        """
119
+        Render a single node with proper tree connectors.
120
+        
121
+        Args:
122
+            node: Node to render
123
+            prefix: Current prefix for indentation
124
+            is_last: Whether this is the last child at this level
125
+            is_root: Whether this is a direct child of root
126
+        """
127
+        # Build the connector
128
+        if is_root:
129
+            # Direct children of root
130
+            connector = self.chars['last'] if is_last else self.chars['branch']
131
+        else:
132
+            connector = self.chars['last'] if is_last else self.chars['branch']
133
+        
134
+        # Get display name
135
+        display_name = self._get_display_name(node.entry)
136
+        
137
+        # Add SELinux context if requested
138
+        selinux_info = ""
139
+        if self.show_selinux:
140
+            selinux_info = self._get_selinux_display(node.entry.path)
141
+            
142
+        # Print the node
143
+        line = f"{prefix}{connector} {display_name}{selinux_info}"
144
+        self._safe_print(line)
145
+        
146
+        # Update statistics
147
+        if node.entry.is_dir:
148
+            self.dir_count += 1
149
+        else:
150
+            self.file_count += 1
151
+        
152
+        # Render children if any
153
+        if node.children:
154
+            # Build prefix for children
155
+            if is_last:
156
+                child_prefix = prefix + self.chars['space']
157
+            else:
158
+                child_prefix = prefix + self.chars['continuation']
159
+            
160
+            self._render_node_children(node.children, child_prefix)
161
+    
162
+    def _get_display_name(self, entry: FileEntry) -> str:
163
+        """
164
+        Get the display name for a file entry.
165
+        
166
+        Args:
167
+            entry: FileEntry to get name for
168
+            
169
+        Returns:
170
+            Formatted display name
171
+        """
172
+        if self.show_full_path:
173
+            display_name = str(entry.path)
174
+        else:
175
+            display_name = entry.name
176
+            
177
+        # Add symlink target if applicable
178
+        if entry.is_symlink and entry.symlink_target:
179
+            safe_target = self._safe_filename(str(entry.symlink_target))
180
+            display_name += f" -> {safe_target}"
181
+            
182
+        return self._safe_filename(display_name)
183
+    
184
+    def _get_selinux_display(self, file_path: Path) -> str:
185
+        """
186
+        Get formatted SELinux context for display.
187
+        
188
+        Args:
189
+            file_path: Path to get context for
190
+            
191
+        Returns:
192
+            Formatted SELinux context string
193
+        """
194
+        try:
195
+            context = get_selinux_context(file_path)
196
+            if context:
197
+                return f"  [{context}]"
198
+        except Exception as e:
199
+            # Don't let SELinux errors break tree rendering
200
+            pass
201
+            
202
+        return ""
203
+    
204
+    def _safe_filename(self, filename: str) -> str:
205
+        """
206
+        Make filename safe for terminal display.
207
+        
208
+        Args:
209
+            filename: Raw filename
210
+            
211
+        Returns:
212
+            Safely escaped filename
213
+        """
214
+        if not filename:
215
+            return "<empty>"
216
+            
217
+        # Replace control characters with visible representations
218
+        safe_chars = []
219
+        for char in filename:
220
+            # Replace control characters with visible representations
221
+            if ord(char) < 32 or ord(char) == 127:  # Control characters
222
+                safe_chars.append(f"\\x{ord(char):02x}")
223
+            elif char in ('\n', '\r', '\t'):  # Specific dangerous chars
224
+                safe_chars.append(repr(char)[1:-1])  # Use repr but remove quotes
225
+            else:
226
+                safe_chars.append(char)
227
+                
228
+        return ''.join(safe_chars)
229
+    
230
+    def _print_root(self, root_path: Path) -> None:
231
+        """
232
+        Print the root directory name.
233
+        
234
+        Args:
235
+            root_path: Root directory to display
236
+        """
237
+        root_display = str(root_path) if self.show_full_path else root_path.name or str(root_path)
238
+        safe_display = self._safe_filename(root_display)
239
+        self._safe_print(safe_display)
240
+    
241
+    def _safe_print(self, text: str) -> None:
242
+        """
243
+        Safely print text to output, handling encoding issues.
244
+        
245
+        Args:
246
+            text: Text to print
247
+        """
248
+        try:
249
+            print(text, file=self.output_file)
250
+        except UnicodeEncodeError:
251
+            # Fallback for terminals that can't handle the text
252
+            safe_text = text.encode('ascii', 'replace').decode('ascii')
253
+            print(safe_text, file=self.output_file)
254
+    
255
+    def _print_report(self) -> None:
256
+        """
257
+        Print summary report of files and directories.
258
+        """
259
+        total = self.dir_count + self.file_count
260
+        
261
+        report_parts = []
262
+        if self.dir_count > 0:
263
+            dir_word = "directory" if self.dir_count == 1 else "directories"
264
+            report_parts.append(f"{self.dir_count} {dir_word}")
265
+            
266
+        if self.file_count > 0:
267
+            file_word = "file" if self.file_count == 1 else "files"
268
+            report_parts.append(f"{self.file_count} {file_word}")
269
+            
270
+        if report_parts:
271
+            report = ", ".join(report_parts)
272
+            self._safe_print(f"\n{report}")
273
+
274
+
275
+def build_tree_structure(entries: List[FileEntry], root_path: Path) -> TreeNode:
276
+    """
277
+    Build a proper tree structure from a flat list of entries.
278
+    
279
+    Args:
280
+        entries: Flat list of FileEntry objects
281
+        root_path: The root directory path
282
+        
283
+    Returns:
284
+        Root TreeNode with hierarchical structure
285
+    """
286
+    # Create a mapping from path to node
287
+    path_to_node: Dict[Path, TreeNode] = {}
288
+    
289
+    # Create the root node first
290
+    root_entry = FileEntry(
291
+        path=root_path,
292
+        name=root_path.name or str(root_path),
293
+        is_dir=True,
294
+        depth=0
295
+    )
296
+    root_node = TreeNode(entry=root_entry, children=[])
297
+    path_to_node[root_path] = root_node
298
+    
299
+    # Sort entries by path depth to ensure parents are processed before children
300
+    sorted_entries = sorted(entries, key=lambda e: len(e.path.parts))
301
+    
302
+    for entry in sorted_entries:
303
+        # Create node for this entry
304
+        node = TreeNode(entry=entry, children=[])
305
+        path_to_node[entry.path] = node
306
+        
307
+        # Find parent and add as child
308
+        parent_path = entry.path.parent
309
+        if parent_path in path_to_node:
310
+            parent_node = path_to_node[parent_path]
311
+            parent_node.children.append(node)
312
+        else:
313
+            # Parent not in our tree (might be filtered out or permission denied)
314
+            # Try to find the closest ancestor that exists
315
+            current_path = parent_path
316
+            while current_path and current_path != current_path.parent:
317
+                if current_path in path_to_node:
318
+                    path_to_node[current_path].children.append(node)
319
+                    break
320
+                current_path = current_path.parent
321
+            else:
322
+                # No ancestor found, add to root
323
+                root_node.children.append(node)
324
+    
325
+    # Sort children at each level (directories first, then alphabetical)
326
+    def sort_children(node: TreeNode):
327
+        node.children.sort(key=lambda n: (not n.entry.is_dir, n.entry.name.lower()))
328
+        for child in node.children:
329
+            sort_children(child)
330
+    
331
+    sort_children(root_node)
332
+    
333
+    return root_node
src/sultree/validators.pyadded
@@ -0,0 +1,228 @@
1
+"""
2
+Reusable validation utilities for sultree.
3
+
4
+This module consolidates validation logic to eliminate duplication
5
+and provide consistent error handling.
6
+"""
7
+
8
+import argparse
9
+import re
10
+from pathlib import Path
11
+from typing import Union, List
12
+
13
+from .constants import CONFIG
14
+
15
+
16
+class ValidationError(Exception):
17
+    """Base exception for validation errors."""
18
+    pass
19
+
20
+
21
+def validate_integer_in_range(
22
+    value: str, 
23
+    name: str,
24
+    min_val: int = 0, 
25
+    max_val: Union[int, None] = None
26
+) -> int:
27
+    """
28
+    Generic integer validation with range checking.
29
+    
30
+    Args:
31
+        value: String representation of integer
32
+        name: Human-readable name for error messages
33
+        min_val: Minimum allowed value (inclusive)
34
+        max_val: Maximum allowed value (inclusive), None for no limit
35
+        
36
+    Returns:
37
+        Validated integer value
38
+        
39
+    Raises:
40
+        argparse.ArgumentTypeError: If validation fails
41
+    """
42
+    try:
43
+        int_val = int(value)
44
+        
45
+        if int_val < min_val:
46
+            raise argparse.ArgumentTypeError(f"{name} must be >= {min_val}")
47
+            
48
+        if max_val is not None and int_val > max_val:
49
+            raise argparse.ArgumentTypeError(f"{name} too large (max {max_val})")
50
+            
51
+        return int_val
52
+        
53
+    except ValueError:
54
+        raise argparse.ArgumentTypeError(f"{name} must be an integer")
55
+
56
+
57
+def validate_string_length(
58
+    value: str,
59
+    name: str, 
60
+    max_length: int,
61
+    allow_empty: bool = False
62
+) -> str:
63
+    """
64
+    Generic string length validation.
65
+    
66
+    Args:
67
+        value: String to validate
68
+        name: Human-readable name for error messages
69
+        max_length: Maximum allowed length
70
+        allow_empty: Whether empty strings are allowed
71
+        
72
+    Returns:
73
+        Validated string
74
+        
75
+    Raises:
76
+        argparse.ArgumentTypeError: If validation fails
77
+    """
78
+    if not value and not allow_empty:
79
+        raise argparse.ArgumentTypeError(f"{name} cannot be empty")
80
+        
81
+    if len(value) > max_length:
82
+        raise argparse.ArgumentTypeError(
83
+            f"{name} too long (max {max_length} characters)"
84
+        )
85
+        
86
+    return value
87
+
88
+
89
+def validate_pattern_safety(pattern: str, name: str) -> str:
90
+    """
91
+    Validate pattern for potentially dangerous constructs.
92
+    
93
+    Args:
94
+        pattern: Pattern to validate
95
+        name: Human-readable name for error messages
96
+        
97
+    Returns:
98
+        Validated pattern
99
+        
100
+    Raises:
101
+        argparse.ArgumentTypeError: If pattern contains dangerous constructs
102
+    """
103
+    # Check for dangerous regex patterns
104
+    for dangerous in CONFIG.security.DANGEROUS_REGEX_PATTERNS:
105
+        if re.search(dangerous, pattern):
106
+            raise argparse.ArgumentTypeError(
107
+                f"{name} contains potentially dangerous constructs"
108
+            )
109
+    
110
+    return pattern
111
+
112
+
113
+def sanitize_selinux_pattern(pattern: str) -> str:
114
+    """
115
+    Sanitize SELinux pattern by removing dangerous characters.
116
+    
117
+    Args:
118
+        pattern: Pattern to sanitize
119
+        
120
+    Returns:
121
+        Sanitized pattern
122
+    """
123
+    # Remove dangerous characters
124
+    for char in CONFIG.security.DANGEROUS_CHARS:
125
+        pattern = pattern.replace(char, '')
126
+        
127
+    return pattern.strip()
128
+
129
+
130
+def validate_selinux_context_format(pattern: str, name: str) -> str:
131
+    """
132
+    Validate SELinux context pattern format.
133
+    
134
+    Args:
135
+        pattern: SELinux pattern to validate
136
+        name: Human-readable name for error messages
137
+        
138
+    Returns:
139
+        Validated pattern
140
+        
141
+    Raises:
142
+        argparse.ArgumentTypeError: If format is invalid
143
+    """
144
+    # Basic SELinux context format validation
145
+    if ':' in pattern:
146
+        parts = pattern.split(':')
147
+        if len(parts) > CONFIG.limits.MAX_SELINUX_FIELDS:
148
+            raise argparse.ArgumentTypeError(
149
+                f"{name} has too many colon-separated parts"
150
+            )
151
+    
152
+    return pattern
153
+
154
+
155
+def validate_path_safety(path_str: str, name: str) -> Path:
156
+    """
157
+    Validate path for security issues.
158
+    
159
+    Args:
160
+        path_str: Path string to validate
161
+        name: Human-readable name for error messages
162
+        
163
+    Returns:
164
+        Validated Path object
165
+        
166
+    Raises:
167
+        argparse.ArgumentTypeError: If path is unsafe
168
+    """
169
+    # Check for null bytes (major security issue)
170
+    if '\x00' in path_str:
171
+        raise argparse.ArgumentTypeError(f"{name} contains null bytes")
172
+    
173
+    try:
174
+        path = Path(path_str)
175
+        return path
176
+    except (ValueError, OSError) as e:
177
+        raise argparse.ArgumentTypeError(f"invalid {name}: {e}")
178
+
179
+
180
+# Specialized validators using the generic functions above
181
+
182
+def validate_depth(value: str) -> int:
183
+    """Validate depth argument."""
184
+    return validate_integer_in_range(
185
+        value, "depth", 0, CONFIG.limits.MAX_DEPTH
186
+    )
187
+
188
+
189
+def validate_file_limit(value: str) -> int:
190
+    """Validate file limit argument."""
191
+    return validate_integer_in_range(
192
+        value, "file limit", 1, CONFIG.limits.MAX_FILE_LIMIT
193
+    )
194
+
195
+
196
+def validate_pattern(pattern: str) -> str:
197
+    """Validate and sanitize pattern arguments."""
198
+    pattern = validate_string_length(
199
+        pattern, "pattern", CONFIG.limits.MAX_PATTERN_LENGTH
200
+    )
201
+    return validate_pattern_safety(pattern, "pattern")
202
+
203
+
204
+def validate_selinux_pattern(pattern: str) -> str:
205
+    """Validate SELinux pattern for security."""
206
+    pattern = validate_string_length(
207
+        pattern, "SELinux pattern", CONFIG.limits.MAX_SELINUX_PATTERN_LENGTH
208
+    )
209
+    
210
+    # Check for dangerous characters
211
+    for char in CONFIG.security.DANGEROUS_CHARS:
212
+        if char in pattern:
213
+            raise argparse.ArgumentTypeError(
214
+                f"SELinux pattern contains invalid character: '{char}'"
215
+            )
216
+    
217
+    pattern = validate_selinux_context_format(pattern, "SELinux pattern")
218
+    
219
+    return pattern
220
+
221
+
222
+def validate_path(path_str: str) -> Path:
223
+    """Validate and sanitize path arguments."""
224
+    path_str = validate_string_length(
225
+        path_str, "path", CONFIG.limits.MAX_PATH_LENGTH
226
+    )
227
+    
228
+    return validate_path_safety(path_str, "path")
sultree.specmodified
@@ -32,10 +32,12 @@ Features:
32
 %autosetup
32
 %autosetup
33
 
33
 
34
 %build
34
 %build
35
-%py3_build
35
+export PATH=$PATH:/home/espadon/.local/bin
36
+%{python3} -m build --wheel --no-isolation
36
 
37
 
37
 %install
38
 %install
38
-%py3_install
39
+export PATH=$PATH:/home/espadon/.local/bin
40
+%{python3} -m pip install --root %{buildroot} --no-deps --ignore-installed dist/*.whl
39
 
41
 
40
 # Install the standalone script
42
 # Install the standalone script
41
 install -Dm755 sultree %{buildroot}%{_bindir}/sultree-standalone
43
 install -Dm755 sultree %{buildroot}%{_bindir}/sultree-standalone
@@ -43,7 +45,7 @@ install -Dm755 sultree %{buildroot}%{_bindir}/sultree-standalone
43
 %files
45
 %files
44
 %doc README.md
46
 %doc README.md
45
 %{python3_sitelib}/%{name}/
47
 %{python3_sitelib}/%{name}/
46
-%{python3_sitelib}/%{name}-%{version}*.egg-info/
48
+%{python3_sitelib}/%{name}-%{version}*.dist-info/
47
 %{_bindir}/sultree
49
 %{_bindir}/sultree
48
 %{_bindir}/sultree-standalone
50
 %{_bindir}/sultree-standalone
49
 
51