Coverage for ntnlog / ntn_file_utils.py: 100%
49 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 04:50 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 04:50 +0000
1#######################################################################
2#
3# File Utils
4#
5# Utility functions for file and directory operations
6# Handles path verification and error management
7#
8#######################################################################
10import os
11from enum import Enum
14class FileUtilsError(str, Enum):
15 """
16 Error message templates returned by ``file_verify_path`` and
17 ``file_verify_file`` on failure. Each value is a format string; call
18 ``.value.format(...)`` with the appropriate keyword arguments to produce
19 the final message.
21 Members
22 -------
23 OUTSIDE_WORKING_DIR — directory is outside the working directory.
24 NOT_A_DIRECTORY — path exists but is not a directory.
25 FILE_READ_OUTSIDE_WORKING_DIR — read target is outside the working directory.
26 FILE_READ_NOT_FOUND_OR_NOT_REGULAR — read target does not exist or is not a regular file.
27 FILE_WRITE_OUTSIDE_WORKING_DIR — write target is outside the working directory.
28 FILE_WRITE_NOT_FOUND_OR_NOT_REGULAR — write target does not exist or is not a regular file.
29 FILE_EXECUTE_OUTSIDE_WORKING_DIR — execute target is outside the working directory.
30 FILE_EXECUTE_NOT_FOUND_OR_NOT_REGULAR — execute target does not exist.
31 FILE_EXECUTE_NOT_PYTHON — execute target is not a ``.py`` file.
32 """
34 # Directory related errors
35 OUTSIDE_WORKING_DIR = "Error: Cannot list {directory} as it is outside the permitted working directory"
36 NOT_A_DIRECTORY = "Error: {directory} is not a directory"
37 # Read file errors
38 FILE_READ_OUTSIDE_WORKING_DIR = "Error: Cannot read {file_path} as it is outside the permitted working directory"
39 FILE_READ_NOT_FOUND_OR_NOT_REGULAR = 'Error: File not found or is not a regular file: "{file_path}"'
40 # Write file errors
41 FILE_WRITE_OUTSIDE_WORKING_DIR = 'Error: Cannot write to "{file_path}" as it is outside the permitted working directory'
42 FILE_WRITE_NOT_FOUND_OR_NOT_REGULAR = 'Error: File not found or is not a regular file: "{file_path}"\n Create the file before writing to it.'
43 # Executable file errors
44 FILE_EXECUTE_OUTSIDE_WORKING_DIR = 'Error: Cannot execute "{file_path}" as it is outside the permitted working directory'
45 FILE_EXECUTE_NOT_FOUND_OR_NOT_REGULAR = 'Error: File "{file_path}" not found.'
46 FILE_EXECUTE_NOT_PYTHON = 'Error: File "{file_path}" is not a Python file.'
49class FileExecutionError(str, Enum):
50 """
51 Error message templates for Python file execution failures.
53 Members
54 -------
55 EXECUTION_FAILED — the file raised an exception; format with ``error_message``.
56 EXECUTION_TIMEOUT — the file exceeded the time limit; format with ``file_path``
57 and ``timeout``.
58 """
60 EXECUTION_FAILED = "Error: executing Python file: {error_message}"
61 EXECUTION_TIMEOUT = 'Error: Execution of file "{file_path}" timed out after {timeout} seconds.'
64class FileOperator(str, Enum):
65 """
66 Operation selector passed to ``file_verify_file``.
68 Members
69 -------
70 READ_FILE — validate that the file exists and is readable.
71 WRITE_FILE — validate that the path is inside the working directory
72 (the file need not exist yet).
73 EXECUTE_FILE — validate that the file exists and is a ``.py`` file.
74 """
76 READ_FILE = "read_file"
77 WRITE_FILE = "write_file"
78 EXECUTE_FILE = "execute_file"
81def file_verify_path(working_directory: str, directory: str) -> str:
82 """
83 Verify that *directory* exists inside *working_directory*.
85 Parameters
86 ----------
87 working_directory : str
88 Absolute path used as the root boundary.
89 directory : str
90 Path to verify, relative to *working_directory*.
92 Returns
93 -------
94 str
95 Resolved absolute path on success.
96 A ``FileUtilsError`` message string when the path escapes the working
97 directory or does not exist as a directory.
98 """
99 path_to_directory = os.path.join(working_directory, directory)
100 if (
101 not path_to_directory.startswith(working_directory)
102 or ".." in os.path.relpath(path_to_directory, working_directory)
103 ):
104 return FileUtilsError.OUTSIDE_WORKING_DIR.value.format(directory=directory)
106 if not os.path.isdir(path_to_directory):
107 return FileUtilsError.NOT_A_DIRECTORY.value.format(directory=directory)
109 return path_to_directory
112def file_verify_file(working_directory: str, file: str, options: FileOperator | None = None) -> str:
113 """
114 Verify that *file* is valid for the requested *options* operation.
116 Parameters
117 ----------
118 working_directory : str
119 Absolute path used as the root boundary.
120 file : str
121 Path to verify, relative to *working_directory*.
122 options : FileOperator | None
123 Operation being attempted:
125 ``FileOperator.READ_FILE`` — file must exist and be a regular file.
126 ``FileOperator.WRITE_FILE`` — path must be inside the working directory
127 (need not exist yet).
128 ``FileOperator.EXECUTE_FILE`` — file must exist and end with ``.py``.
130 Returns
131 -------
132 str
133 Resolved absolute path on success.
134 A ``FileUtilsError`` message string when validation fails.
136 Raises
137 ------
138 ValueError
139 If *options* is not a recognised ``FileOperator`` value.
140 """
141 path_to_file = os.path.join(working_directory, file)
143 def _is_outside() -> bool:
144 return (
145 not path_to_file.startswith(working_directory)
146 or ".." in os.path.relpath(path_to_file, working_directory)
147 )
149 match options:
150 case FileOperator.READ_FILE:
151 if _is_outside():
152 return FileUtilsError.FILE_READ_OUTSIDE_WORKING_DIR.value.format(file_path=path_to_file)
153 if not os.path.isfile(path_to_file):
154 return FileUtilsError.FILE_READ_NOT_FOUND_OR_NOT_REGULAR.value.format(file_path=path_to_file)
156 case FileOperator.WRITE_FILE:
157 if _is_outside():
158 return FileUtilsError.FILE_WRITE_OUTSIDE_WORKING_DIR.value.format(file_path=path_to_file)
159 # Write operations may target a not-yet-existing file; the caller is
160 # responsible for creating any missing parent directories. We only
161 # validate the path is inside the working directory and return it.
163 case FileOperator.EXECUTE_FILE:
164 if _is_outside():
165 return FileUtilsError.FILE_EXECUTE_OUTSIDE_WORKING_DIR.value.format(file_path=file)
166 if not os.path.isfile(path_to_file):
167 return FileUtilsError.FILE_EXECUTE_NOT_FOUND_OR_NOT_REGULAR.value.format(file_path=file)
168 if not path_to_file.endswith(".py"):
169 return FileUtilsError.FILE_EXECUTE_NOT_PYTHON.value.format(file_path=file)
171 case _:
172 raise ValueError(f"Invalid file operation option provided: {options!r}")
174 return path_to_file # reached by all arms that don't return early