castrel-proxy 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,17 @@
1
+ """File and document operations modules"""
2
+
3
+ from .document import (
4
+ DocumentOperationError,
5
+ edit_document,
6
+ parse_document_args,
7
+ read_document,
8
+ write_document,
9
+ )
10
+
11
+ __all__ = [
12
+ "DocumentOperationError",
13
+ "read_document",
14
+ "write_document",
15
+ "edit_document",
16
+ "parse_document_args",
17
+ ]
@@ -0,0 +1,343 @@
1
+ """
2
+ Document Operations Module
3
+
4
+ Provides document reading, writing, and editing functionality
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # File size limit (10MB)
15
+ MAX_FILE_SIZE = 10 * 1024 * 1024
16
+
17
+
18
+ class DocumentOperationError(Exception):
19
+ """Document operation exceptions"""
20
+
21
+ pass
22
+
23
+
24
+ def _expand_path(file_path: str) -> Path:
25
+ """
26
+ Expand file path, supports ~ and environment variables
27
+
28
+ Args:
29
+ file_path: Original file path
30
+
31
+ Returns:
32
+ Path: Expanded path object
33
+ """
34
+ expanded = os.path.expanduser(os.path.expandvars(file_path))
35
+ return Path(expanded).resolve()
36
+
37
+
38
+ def _validate_path(file_path: Path) -> None:
39
+ """
40
+ Validate path security
41
+
42
+ Args:
43
+ file_path: File path
44
+
45
+ Raises:
46
+ DocumentOperationError: Raised when path is unsafe
47
+ """
48
+ # Check if path is absolute
49
+ if not file_path.is_absolute():
50
+ raise DocumentOperationError(f"Path must be absolute: {file_path}")
51
+
52
+
53
+ def _detect_encoding(file_path: Path) -> str:
54
+ """
55
+ Detect file encoding
56
+
57
+ Args:
58
+ file_path: File path
59
+
60
+ Returns:
61
+ str: Encoding name
62
+ """
63
+ # Try common encodings
64
+ encodings = ["utf-8", "gbk", "gb2312", "latin-1"]
65
+
66
+ for encoding in encodings:
67
+ try:
68
+ with open(file_path, "r", encoding=encoding) as f:
69
+ f.read()
70
+ logger.debug(f"Detected encoding: {encoding} for {file_path}")
71
+ return encoding
72
+ except (UnicodeDecodeError, LookupError):
73
+ continue
74
+
75
+ # Default to utf-8
76
+ logger.warning(f"Could not detect encoding for {file_path}, using utf-8")
77
+ return "utf-8"
78
+
79
+
80
+ def read_document(file_path: str, encoding: Optional[str] = None) -> Dict[str, Any]:
81
+ """
82
+ Read document content
83
+
84
+ Args:
85
+ file_path: File path
86
+ encoding: File encoding, defaults to auto-detect
87
+
88
+ Returns:
89
+ Dict[str, Any]: Execution result
90
+ {
91
+ "success": bool,
92
+ "content": str, # File content
93
+ "encoding": str, # Encoding used
94
+ "size": int, # File size (bytes)
95
+ "error": str # Error message (if failed)
96
+ }
97
+ """
98
+ try:
99
+ # Expand path
100
+ path = _expand_path(file_path)
101
+ logger.info(f"[DOC-READ] Reading document: {path}")
102
+
103
+ # Validate path
104
+ _validate_path(path)
105
+
106
+ # Check if file exists
107
+ if not path.exists():
108
+ return {"success": False, "error": f"File does not exist: {path}"}
109
+
110
+ # Check if it's a file
111
+ if not path.is_file():
112
+ return {"success": False, "error": f"Not a file: {path}"}
113
+
114
+ # Check file size
115
+ file_size = path.stat().st_size
116
+ if file_size > MAX_FILE_SIZE:
117
+ return {
118
+ "success": False,
119
+ "error": f"File too large: {file_size} bytes (max: {MAX_FILE_SIZE} bytes)",
120
+ }
121
+
122
+ # Check read permission
123
+ if not os.access(path, os.R_OK):
124
+ return {"success": False, "error": f"No read permission: {path}"}
125
+
126
+ # Detect encoding
127
+ if encoding is None:
128
+ encoding = _detect_encoding(path)
129
+
130
+ # Read file
131
+ try:
132
+ with open(path, "r", encoding=encoding) as f:
133
+ content = f.read()
134
+ except UnicodeDecodeError:
135
+ logger.warning(f"Failed to decode with {encoding}, trying utf-8 with errors='replace'")
136
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
137
+ content = f.read()
138
+ encoding = "utf-8 (with replacements)"
139
+
140
+ logger.info(f"[DOC-READ-SUCCESS] Read {file_size} bytes from {path}")
141
+
142
+ return {
143
+ "success": True,
144
+ "content": content,
145
+ "encoding": encoding,
146
+ "size": file_size,
147
+ }
148
+
149
+ except DocumentOperationError as e:
150
+ logger.error(f"[DOC-READ-ERROR] Validation error: {e}")
151
+ return {"success": False, "error": str(e)}
152
+ except Exception as e:
153
+ logger.error(f"[DOC-READ-ERROR] Unexpected error: {e}", exc_info=True)
154
+ return {"success": False, "error": f"Failed to read file: {str(e)}"}
155
+
156
+
157
+ def write_document(file_path: str, content: str, encoding: str = "utf-8", create_dirs: bool = True) -> Dict[str, Any]:
158
+ """
159
+ Write document content (overwrite mode)
160
+
161
+ Args:
162
+ file_path: File path
163
+ content: File content
164
+ encoding: File encoding, defaults to utf-8
165
+ create_dirs: Whether to automatically create parent directories, defaults to True
166
+
167
+ Returns:
168
+ Dict[str, Any]: Execution result
169
+ {
170
+ "success": bool,
171
+ "size": int, # Number of bytes written
172
+ "path": str, # File path
173
+ "error": str # Error message (if failed)
174
+ }
175
+ """
176
+ try:
177
+ # Expand path
178
+ path = _expand_path(file_path)
179
+ logger.info(f"[DOC-WRITE] Writing document: {path}")
180
+
181
+ # Validate path
182
+ _validate_path(path)
183
+
184
+ # Create parent directories
185
+ if create_dirs:
186
+ path.parent.mkdir(parents=True, exist_ok=True)
187
+ elif not path.parent.exists():
188
+ return {"success": False, "error": f"Parent directory does not exist: {path.parent}"}
189
+
190
+ # Check parent directory write permission
191
+ if not os.access(path.parent, os.W_OK):
192
+ return {"success": False, "error": f"No write permission: {path.parent}"}
193
+
194
+ # If file exists, check write permission
195
+ if path.exists() and not os.access(path, os.W_OK):
196
+ return {"success": False, "error": f"No write permission: {path}"}
197
+
198
+ # Write file
199
+ with open(path, "w", encoding=encoding) as f:
200
+ f.write(content)
201
+
202
+ # Get file size after writing
203
+ file_size = path.stat().st_size
204
+
205
+ logger.info(f"[DOC-WRITE-SUCCESS] Wrote {file_size} bytes to {path}")
206
+
207
+ return {
208
+ "success": True,
209
+ "size": file_size,
210
+ "path": str(path),
211
+ }
212
+
213
+ except DocumentOperationError as e:
214
+ logger.error(f"[DOC-WRITE-ERROR] Validation error: {e}")
215
+ return {"success": False, "error": str(e)}
216
+ except Exception as e:
217
+ logger.error(f"[DOC-WRITE-ERROR] Unexpected error: {e}", exc_info=True)
218
+ return {"success": False, "error": f"Failed to write file: {str(e)}"}
219
+
220
+
221
+ def edit_document(
222
+ file_path: str,
223
+ operation: str,
224
+ new_content: str,
225
+ old_content: Optional[str] = None,
226
+ encoding: Optional[str] = None,
227
+ ) -> Dict[str, Any]:
228
+ """
229
+ Edit document content
230
+
231
+ Args:
232
+ file_path: File path
233
+ operation: Operation type - "replace" (replace), "append" (append), "prepend" (prepend)
234
+ new_content: New content
235
+ old_content: Old content (only needed for replace operation)
236
+ encoding: File encoding, defaults to auto-detect
237
+
238
+ Returns:
239
+ Dict[str, Any]: Execution result
240
+ {
241
+ "success": bool,
242
+ "size": int, # File size after editing
243
+ "operation": str, # Operation performed
244
+ "path": str, # File path
245
+ "error": str # Error message (if failed)
246
+ }
247
+ """
248
+ try:
249
+ # Expand path
250
+ path = _expand_path(file_path)
251
+ logger.info(f"[DOC-EDIT] Editing document: {path}, operation: {operation}")
252
+
253
+ # Validate path
254
+ _validate_path(path)
255
+
256
+ # Check operation type
257
+ if operation not in ["replace", "append", "prepend"]:
258
+ return {"success": False, "error": f"Unsupported operation type: {operation}"}
259
+
260
+ # Check if file exists
261
+ if not path.exists():
262
+ return {"success": False, "error": f"File does not exist: {path}"}
263
+
264
+ # Check if it's a file
265
+ if not path.is_file():
266
+ return {"success": False, "error": f"Not a file: {path}"}
267
+
268
+ # Check read/write permissions
269
+ if not os.access(path, os.R_OK):
270
+ return {"success": False, "error": f"No read permission: {path}"}
271
+ if not os.access(path, os.W_OK):
272
+ return {"success": False, "error": f"No write permission: {path}"}
273
+
274
+ # Read existing content
275
+ read_result = read_document(str(path), encoding)
276
+ if not read_result["success"]:
277
+ return read_result
278
+
279
+ current_content = read_result["content"]
280
+ detected_encoding = read_result["encoding"]
281
+
282
+ # Perform edit operation
283
+ if operation == "replace":
284
+ if old_content is None:
285
+ return {"success": False, "error": "replace operation requires old_content parameter"}
286
+
287
+ if old_content not in current_content:
288
+ return {"success": False, "error": f"Content to replace not found: {old_content[:50]}..."}
289
+
290
+ # Replace content
291
+ new_file_content = current_content.replace(old_content, new_content)
292
+
293
+ elif operation == "append":
294
+ # Append to end of file
295
+ new_file_content = current_content + new_content
296
+
297
+ elif operation == "prepend":
298
+ # Insert at beginning of file
299
+ new_file_content = new_content + current_content
300
+
301
+ # Write edited content
302
+ write_result = write_document(str(path), new_file_content, detected_encoding.split()[0], create_dirs=False)
303
+
304
+ if write_result["success"]:
305
+ write_result["operation"] = operation
306
+ logger.info(f"[DOC-EDIT-SUCCESS] Edited {path} with {operation} operation")
307
+
308
+ return write_result
309
+
310
+ except DocumentOperationError as e:
311
+ logger.error(f"[DOC-EDIT-ERROR] Validation error: {e}")
312
+ return {"success": False, "error": str(e)}
313
+ except Exception as e:
314
+ logger.error(f"[DOC-EDIT-ERROR] Unexpected error: {e}", exc_info=True)
315
+ return {"success": False, "error": f"Failed to edit file: {str(e)}"}
316
+
317
+
318
+ def parse_document_args(args: list) -> Dict[str, Any]:
319
+ """
320
+ Parse document operation arguments
321
+
322
+ Args:
323
+ args: Argument list, format like ["--file", "/path/to/file", "--content", "..."]
324
+
325
+ Returns:
326
+ Dict[str, Any]: Parsed arguments dictionary
327
+ """
328
+ params = {}
329
+ i = 0
330
+ while i < len(args):
331
+ arg = args[i]
332
+ if arg.startswith("--"):
333
+ key = arg[2:] # Remove "--" prefix
334
+ if i + 1 < len(args) and not args[i + 1].startswith("--"):
335
+ params[key] = args[i + 1]
336
+ i += 2
337
+ else:
338
+ params[key] = True
339
+ i += 1
340
+ else:
341
+ i += 1
342
+
343
+ return params
@@ -0,0 +1,17 @@
1
+ """Security features for Castrel Bridge Proxy"""
2
+
3
+ from .whitelist import (
4
+ get_default_whitelist,
5
+ get_whitelist_file_path,
6
+ init_whitelist_file,
7
+ is_command_allowed,
8
+ load_whitelist,
9
+ )
10
+
11
+ __all__ = [
12
+ "get_default_whitelist",
13
+ "load_whitelist",
14
+ "is_command_allowed",
15
+ "get_whitelist_file_path",
16
+ "init_whitelist_file",
17
+ ]