zexus 1.6.2 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -128,15 +128,35 @@ def disable_memory_tracking():
128
128
  # PERSISTENT STORAGE BACKEND
129
129
  # ===============================================
130
130
 
131
+ class StorageLimitError(Exception):
132
+ """Raised when persistent storage limits are exceeded"""
133
+ pass
134
+
135
+
131
136
  class PersistentStorage:
132
- """Persistent storage for variables using SQLite"""
137
+ """Persistent storage for variables using SQLite with size limits"""
133
138
 
134
- def __init__(self, scope_id: str, storage_dir: str = PERSISTENCE_DIR):
139
+ # Default limits (configurable)
140
+ DEFAULT_MAX_ITEMS = 10000 # Maximum number of stored variables
141
+ DEFAULT_MAX_SIZE_MB = 100 # Maximum storage size in MB
142
+
143
+ def __init__(self, scope_id: str, storage_dir: str = PERSISTENCE_DIR,
144
+ max_items: int = None, max_size_mb: int = None):
135
145
  self.scope_id = scope_id
136
146
  self.db_path = os.path.join(storage_dir, f"{scope_id}.sqlite")
137
147
  self.conn = None
138
148
  self.lock = Lock()
149
+
150
+ # Storage limits
151
+ self.max_items = max_items if max_items is not None else self.DEFAULT_MAX_ITEMS
152
+ self.max_size_bytes = (max_size_mb if max_size_mb is not None else self.DEFAULT_MAX_SIZE_MB) * 1024 * 1024
153
+
154
+ # Usage tracking
155
+ self.current_item_count = 0
156
+ self.current_size_bytes = 0
157
+
139
158
  self._init_db()
159
+ self._update_usage_stats()
140
160
 
141
161
  def _init_db(self):
142
162
  """Initialize SQLite database"""
@@ -147,6 +167,7 @@ class PersistentStorage:
147
167
  name TEXT PRIMARY KEY,
148
168
  type TEXT NOT NULL,
149
169
  value TEXT NOT NULL,
170
+ size_bytes INTEGER DEFAULT 0,
150
171
  is_const INTEGER DEFAULT 0,
151
172
  created_at REAL NOT NULL,
152
173
  updated_at REAL NOT NULL
@@ -157,24 +178,96 @@ class PersistentStorage:
157
178
  ''')
158
179
  self.conn.commit()
159
180
 
181
+ def _update_usage_stats(self):
182
+ """Update current usage statistics"""
183
+ with self.lock:
184
+ cursor = self.conn.cursor()
185
+
186
+ # Count items
187
+ cursor.execute('SELECT COUNT(*) FROM variables')
188
+ self.current_item_count = cursor.fetchone()[0]
189
+
190
+ # Calculate total size
191
+ cursor.execute('SELECT SUM(size_bytes) FROM variables')
192
+ result = cursor.fetchone()[0]
193
+ self.current_size_bytes = result if result else 0
194
+
195
+ def _calculate_size(self, serialized: Dict[str, str]) -> int:
196
+ """Calculate size of serialized data in bytes"""
197
+ # Approximate size: length of type + value strings
198
+ size = len(serialized['type']) + len(serialized['value'])
199
+ return size
200
+
201
+ def _check_limits(self, name: str, new_size: int) -> None:
202
+ """Check if adding/updating a variable would exceed limits"""
203
+ # Get current size of existing variable if it exists
204
+ cursor = self.conn.cursor()
205
+ cursor.execute('SELECT size_bytes FROM variables WHERE name = ?', (name,))
206
+ row = cursor.fetchone()
207
+ existing_size = row[0] if row else 0
208
+ is_update = row is not None
209
+
210
+ # Calculate new totals
211
+ new_item_count = self.current_item_count if is_update else self.current_item_count + 1
212
+ new_total_size = self.current_size_bytes - existing_size + new_size
213
+
214
+ # Check item limit
215
+ if new_item_count > self.max_items:
216
+ raise StorageLimitError(
217
+ f"Persistent storage item limit exceeded: {new_item_count} > {self.max_items}. "
218
+ f"Cannot store variable '{name}'. "
219
+ f"Consider increasing max_items or cleaning up old variables."
220
+ )
221
+
222
+ # Check size limit
223
+ if new_total_size > self.max_size_bytes:
224
+ size_mb = new_total_size / (1024 * 1024)
225
+ limit_mb = self.max_size_bytes / (1024 * 1024)
226
+ raise StorageLimitError(
227
+ f"Persistent storage size limit exceeded: {size_mb:.2f}MB > {limit_mb:.2f}MB. "
228
+ f"Cannot store variable '{name}' ({new_size} bytes). "
229
+ f"Consider increasing max_size_mb or cleaning up old data."
230
+ )
231
+
232
+ def get_usage_stats(self) -> Dict[str, Any]:
233
+ """Get current storage usage statistics"""
234
+ return {
235
+ 'item_count': self.current_item_count,
236
+ 'max_items': self.max_items,
237
+ 'items_remaining': self.max_items - self.current_item_count,
238
+ 'size_bytes': self.current_size_bytes,
239
+ 'size_mb': self.current_size_bytes / (1024 * 1024),
240
+ 'max_size_mb': self.max_size_bytes / (1024 * 1024),
241
+ 'size_remaining_mb': (self.max_size_bytes - self.current_size_bytes) / (1024 * 1024),
242
+ 'usage_percent': (self.current_size_bytes / self.max_size_bytes * 100) if self.max_size_bytes > 0 else 0
243
+ }
244
+
160
245
  def set(self, name: str, value: Object, is_const: bool = False):
161
- """Persist a variable"""
246
+ """Persist a variable with size limit checks"""
162
247
  with self.lock:
163
248
  serialized = self._serialize(value)
249
+ size_bytes = self._calculate_size(serialized)
250
+
251
+ # Check limits before storing
252
+ self._check_limits(name, size_bytes)
253
+
164
254
  cursor = self.conn.cursor()
165
255
 
166
256
  import time
167
257
  timestamp = time.time()
168
258
 
169
259
  cursor.execute('''
170
- INSERT OR REPLACE INTO variables (name, type, value, is_const, created_at, updated_at)
171
- VALUES (?, ?, ?, ?,
260
+ INSERT OR REPLACE INTO variables (name, type, value, size_bytes, is_const, created_at, updated_at)
261
+ VALUES (?, ?, ?, ?, ?,
172
262
  COALESCE((SELECT created_at FROM variables WHERE name = ?), ?),
173
263
  ?)
174
- ''', (name, serialized['type'], serialized['value'], 1 if is_const else 0,
264
+ ''', (name, serialized['type'], serialized['value'], size_bytes, 1 if is_const else 0,
175
265
  name, timestamp, timestamp))
176
266
 
177
267
  self.conn.commit()
268
+
269
+ # Update usage stats
270
+ self._update_usage_stats()
178
271
 
179
272
  def get(self, name: str) -> Optional[Object]:
180
273
  """Retrieve a persisted variable"""
@@ -186,6 +279,9 @@ class PersistentStorage:
186
279
  if row is None:
187
280
  return None
188
281
 
282
+
283
+ # Update usage stats
284
+ self._update_usage_stats()
189
285
  return self._deserialize({'type': row[0], 'value': row[1]})
190
286
 
191
287
  def delete(self, name: str):
@@ -213,6 +309,9 @@ class PersistentStorage:
213
309
  def clear(self):
214
310
  """Clear all persisted variables"""
215
311
  with self.lock:
312
+
313
+ # Update usage stats
314
+ self._update_usage_stats()
216
315
  cursor = self.conn.cursor()
217
316
  cursor.execute('DELETE FROM variables')
218
317
  self.conn.commit()
@@ -0,0 +1,237 @@
1
+ # src/zexus/security_enforcement.py
2
+ """
3
+ Security enforcement for Zexus language.
4
+
5
+ This module enforces mandatory sanitization in sensitive contexts.
6
+ It's NOT optional - security is built into the language.
7
+ """
8
+
9
+ from .object import String, EvaluationError
10
+
11
+
12
+ class SecurityEnforcementError(Exception):
13
+ """Raised when unsanitized input is used in sensitive context"""
14
+ pass
15
+
16
+
17
+ class SensitiveContext:
18
+ """Defines sensitive contexts that require sanitization"""
19
+
20
+ SQL = 'sql'
21
+ HTML = 'html'
22
+ URL = 'url'
23
+ SHELL = 'shell'
24
+
25
+ # Patterns that indicate SQL context
26
+ SQL_PATTERNS = [
27
+ 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE',
28
+ 'ALTER', 'FROM', 'WHERE', 'JOIN', 'UNION'
29
+ ]
30
+
31
+ # Patterns that indicate HTML context
32
+ HTML_PATTERNS = [
33
+ '<html', '<div', '<span', '<script', '<body', '<head',
34
+ 'innerHTML', 'outerHTML'
35
+ ]
36
+
37
+ # Patterns that indicate URL context
38
+ URL_PATTERNS = [
39
+ 'http://', 'https://', 'ftp://', '?', '&', 'url=', 'redirect='
40
+ ]
41
+
42
+ # Patterns that indicate shell context
43
+ SHELL_PATTERNS = [
44
+ 'exec', 'system', 'shell', 'bash', 'sh', 'cmd', 'powershell'
45
+ ]
46
+
47
+
48
+ def detect_sensitive_context(string_value):
49
+ """
50
+ Detect if a string is being used in a sensitive context.
51
+
52
+ Returns the context type (sql, html, url, shell) or None.
53
+
54
+ IMPORTANT: This now uses more sophisticated pattern matching to reduce
55
+ false positives. We look for actual dangerous patterns, not just keywords.
56
+ """
57
+ if not isinstance(string_value, str):
58
+ return None
59
+
60
+ upper_value = string_value.upper()
61
+
62
+ # Check for SQL context - require actual SQL query patterns, not just keywords
63
+ # Look for patterns like "SELECT ... FROM", "WHERE ... =", etc.
64
+ sql_query_indicators = [
65
+ ('SELECT', 'FROM'), # SELECT must be followed by FROM
66
+ ('INSERT', 'INTO'), # INSERT must be followed by INTO
67
+ ('UPDATE', 'SET'), # UPDATE must be followed by SET
68
+ ('DELETE', 'FROM'), # DELETE must be followed by FROM
69
+ ('DROP', 'TABLE'), # DROP must be followed by TABLE
70
+ ('CREATE', 'TABLE'), # CREATE must be followed by TABLE
71
+ ]
72
+
73
+ for keyword1, keyword2 in sql_query_indicators:
74
+ if keyword1 in upper_value and keyword2 in upper_value:
75
+ # Found a real SQL query pattern
76
+ return SensitiveContext.SQL
77
+
78
+ # Single keywords alone are not enough - they could be normal text
79
+ # Only trigger if we see SQL-like syntax patterns
80
+ if ' WHERE ' in upper_value and ('=' in string_value or 'LIKE' in upper_value):
81
+ return SensitiveContext.SQL
82
+
83
+ # Check for HTML context - require actual HTML tags, not just keywords
84
+ for pattern in SensitiveContext.HTML_PATTERNS:
85
+ if pattern.lower() in string_value.lower():
86
+ # Check if it's actually a tag (starts with <)
87
+ if pattern.startswith('<') or 'innerHTML' in string_value or 'outerHTML' in string_value:
88
+ return SensitiveContext.HTML
89
+
90
+ # Check for URL context - require actual URL schemes or injection patterns
91
+ url_indicators = ['http://', 'https://', 'ftp://']
92
+ injection_indicators = ['url=', 'redirect=', 'goto=', 'next=']
93
+
94
+ has_url_scheme = any(indicator in string_value.lower() for indicator in url_indicators)
95
+ has_injection_param = any(indicator in string_value.lower() for indicator in injection_indicators)
96
+
97
+ if has_url_scheme or (has_injection_param and ('?' in string_value or '&' in string_value)):
98
+ return SensitiveContext.URL
99
+
100
+ # Check for shell context - require actual command execution patterns
101
+ shell_execution_funcs = ['exec(', 'system(', 'shell(', 'bash ', 'sh ', 'cmd ', 'powershell ']
102
+ if any(pattern in string_value.lower() for pattern in shell_execution_funcs):
103
+ return SensitiveContext.SHELL
104
+
105
+ return None
106
+
107
+
108
+ def enforce_sanitization(string_obj, operation_context=None):
109
+ """
110
+ Enforce sanitization requirement for String objects in sensitive contexts.
111
+
112
+ This is ALWAYS enforced - not optional. Security is built into the language.
113
+
114
+ Args:
115
+ string_obj: The String object to check
116
+ operation_context: Optional explicit context (sql, html, url, shell)
117
+
118
+ Raises:
119
+ EvaluationError: If unsanitized input is used in sensitive context
120
+ """
121
+ if not isinstance(string_obj, String):
122
+ return # Not a string, nothing to enforce
123
+
124
+ # If string is trusted (literal), no enforcement needed
125
+ if string_obj.is_trusted:
126
+ return
127
+
128
+ # Detect context if not explicitly provided
129
+ if operation_context is None:
130
+ operation_context = detect_sensitive_context(string_obj.value)
131
+
132
+ # If no sensitive context detected, allow
133
+ if operation_context is None:
134
+ return
135
+
136
+ # Check if string is sanitized for this context
137
+ if not string_obj.is_safe_for(operation_context):
138
+ raise_sanitization_error(string_obj, operation_context)
139
+
140
+
141
+ def raise_sanitization_error(string_obj, context):
142
+ """
143
+ Raise a clear, helpful error message for unsanitized input.
144
+
145
+ The error message guides developers to use the sanitize keyword.
146
+ """
147
+ context_name = context.upper()
148
+
149
+ # Create helpful error message
150
+ error_msg = f"""
151
+ 🔒 SECURITY ERROR: Unsanitized input used in {context_name} context
152
+
153
+ The string value appears to be used in a {context_name} operation, but it has not been sanitized.
154
+ This could lead to {get_vulnerability_name(context)} vulnerabilities.
155
+
156
+ To fix this, sanitize the input before use:
157
+
158
+ sanitize your_variable as {context}
159
+
160
+ Example:
161
+
162
+ ❌ UNSAFE:
163
+ query = "SELECT * FROM users WHERE name = '" + user_input + "'"
164
+
165
+ ✅ SAFE:
166
+ sanitize user_input as {context}
167
+ query = "SELECT * FROM users WHERE name = '" + user_input + "'"
168
+
169
+ Security is mandatory in Zexus - this protection cannot be disabled.
170
+ """
171
+
172
+ raise SecurityEnforcementError(error_msg.strip())
173
+
174
+
175
+ def get_vulnerability_name(context):
176
+ """Get the vulnerability name for a given context"""
177
+ vuln_map = {
178
+ SensitiveContext.SQL: "SQL Injection",
179
+ SensitiveContext.HTML: "Cross-Site Scripting (XSS)",
180
+ SensitiveContext.URL: "URL Injection / Open Redirect",
181
+ SensitiveContext.SHELL: "Command Injection"
182
+ }
183
+ return vuln_map.get(context, "Injection")
184
+
185
+
186
+ def check_string_concatenation(left, right):
187
+ """
188
+ Check string concatenation for security issues.
189
+
190
+ When concatenating strings, if the result would be used in a sensitive
191
+ context, both operands must be sanitized or trusted.
192
+
193
+ Improvements:
194
+ - If BOTH operands are trusted (literals), the result is safe
195
+ - Only check context on the final combined result
196
+ - Reduce false positives from normal text operations
197
+ """
198
+ # If either operand is a String object, check sanitization
199
+ left_is_string = isinstance(left, String)
200
+ right_is_string = isinstance(right, String)
201
+
202
+ if not (left_is_string or right_is_string):
203
+ return # Not string concatenation
204
+
205
+ # OPTIMIZATION: If both are trusted literals, the concatenation is safe
206
+ if (left_is_string and left.is_trusted) and (right_is_string and right.is_trusted):
207
+ return # Both sides are literals - safe!
208
+
209
+ # Get the concatenated value for context detection
210
+ left_val = left.value if left_is_string else str(left.inspect() if hasattr(left, 'inspect') else left)
211
+ right_val = right.value if right_is_string else str(right.inspect() if hasattr(right, 'inspect') else right)
212
+ combined = left_val + right_val
213
+
214
+ # Detect if the combined string is in a sensitive context
215
+ context = detect_sensitive_context(combined)
216
+
217
+ if context is None:
218
+ return # No sensitive context detected
219
+
220
+ # Check if both operands are safe for this context
221
+ # NOTE: We only enforce if the string is NOT trusted AND NOT sanitized
222
+ if left_is_string and not left.is_trusted and not left.is_safe_for(context):
223
+ enforce_sanitization(left, context)
224
+
225
+ if right_is_string and not right.is_trusted and not right.is_safe_for(context):
226
+ enforce_sanitization(right, context)
227
+
228
+
229
+ def mark_as_trusted(string_obj):
230
+ """
231
+ Mark a string as trusted (from literal, not external input).
232
+
233
+ This should be called when creating String objects from literals.
234
+ """
235
+ if isinstance(string_obj, String):
236
+ string_obj.is_trusted = True
237
+ return string_obj
@@ -4,76 +4,173 @@ import os
4
4
  import shutil
5
5
  import glob as glob_module
6
6
  from pathlib import Path
7
- from typing import List, Dict, Any
7
+ from typing import List, Dict, Any, Optional
8
+
9
+
10
+ class PathTraversalError(Exception):
11
+ """Raised when path traversal attack is detected."""
12
+ pass
8
13
 
9
14
 
10
15
  class FileSystemModule:
11
- """Provides file system operations."""
16
+ """Provides file system operations with path traversal protection."""
17
+
18
+ # Allowed base directories for file operations
19
+ # If None, uses current working directory
20
+ _allowed_base_dirs: Optional[List[str]] = None
21
+ _strict_mode: bool = True # Enable path validation by default
22
+
23
+ @classmethod
24
+ def configure_security(cls, allowed_dirs: Optional[List[str]] = None, strict: bool = True):
25
+ """
26
+ Configure file system security settings.
27
+
28
+ Args:
29
+ allowed_dirs: List of allowed base directories. None = use CWD only.
30
+ strict: Enable strict path validation
31
+ """
32
+ cls._allowed_base_dirs = allowed_dirs
33
+ cls._strict_mode = strict
34
+
35
+ @classmethod
36
+ def _validate_path(cls, path: str, operation: str = "access") -> str:
37
+ """
38
+ Validate path to prevent traversal attacks.
39
+
40
+ Args:
41
+ path: User-provided path
42
+ operation: Type of operation (for error messages)
43
+
44
+ Returns:
45
+ Validated absolute path
46
+
47
+ Raises:
48
+ PathTraversalError: If path traversal detected
49
+ """
50
+ if not cls._strict_mode:
51
+ return path
52
+
53
+ # Convert to absolute path
54
+ abs_path = Path(path).resolve()
55
+
56
+ # Check for common traversal patterns
57
+ path_str = str(path)
58
+ if '..' in path_str:
59
+ # Allow .. only if it doesn't escape allowed directories
60
+ pass # Will be checked below
61
+
62
+ # Determine allowed base directories
63
+ if cls._allowed_base_dirs is None:
64
+ # Default: only allow access within CWD
65
+ allowed_bases = [Path.cwd().resolve()]
66
+ else:
67
+ allowed_bases = [Path(d).resolve() for d in cls._allowed_base_dirs]
68
+
69
+ # Check if resolved path is within allowed directories
70
+ is_allowed = False
71
+ for base in allowed_bases:
72
+ try:
73
+ abs_path.relative_to(base)
74
+ is_allowed = True
75
+ break
76
+ except ValueError:
77
+ continue
78
+
79
+ if not is_allowed:
80
+ raise PathTraversalError(
81
+ f"Path traversal detected: '{path}' resolves to '{abs_path}' "
82
+ f"which is outside allowed directories. "
83
+ f"Allowed: {[str(b) for b in allowed_bases]}"
84
+ )
85
+
86
+ return str(abs_path)
12
87
 
13
88
  @staticmethod
14
89
  def read_file(path: str, encoding: str = 'utf-8') -> str:
15
90
  """Read entire file as text."""
16
- with open(path, 'r', encoding=encoding) as f:
91
+ validated_path = FileSystemModule._validate_path(path, "read")
92
+ with open(validated_path, 'r', encoding=encoding) as f:
17
93
  return f.read()
18
94
 
19
95
  @staticmethod
20
96
  def write_file(path: str, content: str, encoding: str = 'utf-8') -> None:
21
97
  """Write text to file."""
98
+ validated_path = FileSystemModule._validate_path(path, "write")
22
99
  # Create parent directory if it doesn't exist
23
- Path(path).parent.mkdir(parents=True, exist_ok=True)
24
- with open(path, 'w', encoding=encoding) as f:
100
+ Path(validated_path).parent.mkdir(parents=True, exist_ok=True)
101
+ with open(validated_path, 'w', encoding=encoding) as f:
25
102
  f.write(content)
26
103
 
27
104
  @staticmethod
28
105
  def append_file(path: str, content: str, encoding: str = 'utf-8') -> None:
29
106
  """Append text to file."""
107
+ validated_path = FileSystemModule._validate_path(path, "append")
30
108
  # Create parent directory if it doesn't exist (for consistency with write_file)
31
- Path(path).parent.mkdir(parents=True, exist_ok=True)
32
- with open(path, 'a', encoding=encoding) as f:
109
+ Path(validated_path).parent.mkdir(parents=True, exist_ok=True)
110
+ with open(validated_path, 'a', encoding=encoding) as f:
33
111
  f.write(content)
34
112
 
35
113
  @staticmethod
36
114
  def read_binary(path: str) -> bytes:
37
115
  """Read file as binary."""
38
- with open(path, 'rb') as f:
116
+ validated_path = FileSystemModule._validate_path(path, "read_binary")
117
+ with open(validated_path, 'rb') as f:
39
118
  return f.read()
40
119
 
41
120
  @staticmethod
42
121
  def write_binary(path: str, data: bytes) -> None:
43
122
  """Write binary data to file."""
44
- Path(path).parent.mkdir(parents=True, exist_ok=True)
45
- with open(path, 'wb') as f:
123
+ validated_path = FileSystemModule._validate_path(path, "write_binary")
124
+ Path(validated_path).parent.mkdir(parents=True, exist_ok=True)
125
+ with open(validated_path, 'wb') as f:
46
126
  f.write(data)
47
127
 
48
128
  @staticmethod
49
129
  def exists(path: str) -> bool:
50
130
  """Check if file or directory exists."""
51
- return os.path.exists(path)
131
+ try:
132
+ validated_path = FileSystemModule._validate_path(path, "exists")
133
+ return os.path.exists(validated_path)
134
+ except PathTraversalError:
135
+ return False # Return False for invalid paths instead of error
52
136
 
53
137
  @staticmethod
54
138
  def is_file(path: str) -> bool:
55
139
  """Check if path is a file."""
56
- return os.path.isfile(path)
140
+ try:
141
+ validated_path = FileSystemModule._validate_path(path, "is_file")
142
+ return os.path.isfile(validated_path)
143
+ except PathTraversalError:
144
+ return False
57
145
 
58
146
  @staticmethod
59
147
  def is_dir(path: str) -> bool:
60
148
  """Check if path is a directory."""
61
- return os.path.isdir(path)
149
+ try:
150
+ validated_path = FileSystemModule._validate_path(path, "is_dir")
151
+ return os.path.isdir(validated_path)
152
+ except PathTraversalError:
153
+ return False
62
154
 
63
155
  @staticmethod
64
156
  def mkdir(path: str, parents: bool = True) -> None:
65
- """Create directory."""
66
- Path(path).mkdir(parents=parents, exist_ok=True)
157
+ validated_path = FileSystemModule._validate_path(path, "remove")
158
+ os.remove(validated_path)
67
159
 
68
160
  @staticmethod
69
- def rmdir(path: str, recursive: bool = False) -> None:
70
- """Remove directory."""
71
- if recursive:
72
- shutil.rmtree(path)
73
- else:
74
- os.rmdir(path)
161
+ def rename(old_path: str, new_path: str) -> None:
162
+ """Rename/move file or directory."""
163
+ validated_old = FileSystemModule._validate_path(old_path, "rename_source")
164
+ validated_new = FileSystemModule._validate_path(new_path, "rename_dest")
165
+ os.rename(validated_old, validated_new)
75
166
 
76
167
  @staticmethod
168
+ def copy_file(src: str, dst: str) -> None:
169
+ """Copy file."""
170
+ validated_src = FileSystemModule._validate_path(src, "copy_source")
171
+ validated_dst = FileSystemModule._validate_path(dst, "copy_dest")
172
+ shutil.copy2(validated_src, validated_
173
+ @staticmethod
77
174
  def remove(path: str) -> None:
78
175
  """Remove file."""
79
176
  os.remove(path)
@@ -91,7 +188,8 @@ class FileSystemModule:
91
188
  @staticmethod
92
189
  def copy_dir(src: str, dst: str) -> None:
93
190
  """Copy directory recursively."""
94
- shutil.copytree(src, dst)
191
+ validated_path = FileSystemModule._validate_path(path, "list_dir")
192
+ return os.listdir(validated_c, dst)
95
193
 
96
194
  @staticmethod
97
195
  def list_dir(path: str = '.') -> List[str]:
@@ -23,7 +23,7 @@ class PackageManager:
23
23
  self.installer = PackageInstaller(self.zpm_dir)
24
24
  self.publisher = PackagePublisher(self.registry)
25
25
 
26
- def init(self, name: str = None, version: str = "1.6.2") -> Dict:
26
+ def init(self, name: str = None, version: str = "1.6.3") -> Dict:
27
27
  """Initialize a new Zexus project with package.json"""
28
28
  if self.config_file.exists():
29
29
  print(f"⚠️ {self.config_file} already exists")