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

1####################################################################### 

2# 

3# File Utils 

4# 

5# Utility functions for file and directory operations 

6# Handles path verification and error management 

7# 

8####################################################################### 

9 

10import os 

11from enum import Enum 

12 

13 

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. 

20 

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 """ 

33 

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.' 

47 

48 

49class FileExecutionError(str, Enum): 

50 """ 

51 Error message templates for Python file execution failures. 

52 

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 """ 

59 

60 EXECUTION_FAILED = "Error: executing Python file: {error_message}" 

61 EXECUTION_TIMEOUT = 'Error: Execution of file "{file_path}" timed out after {timeout} seconds.' 

62 

63 

64class FileOperator(str, Enum): 

65 """ 

66 Operation selector passed to ``file_verify_file``. 

67 

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 """ 

75 

76 READ_FILE = "read_file" 

77 WRITE_FILE = "write_file" 

78 EXECUTE_FILE = "execute_file" 

79 

80 

81def file_verify_path(working_directory: str, directory: str) -> str: 

82 """ 

83 Verify that *directory* exists inside *working_directory*. 

84 

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*. 

91 

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) 

105 

106 if not os.path.isdir(path_to_directory): 

107 return FileUtilsError.NOT_A_DIRECTORY.value.format(directory=directory) 

108 

109 return path_to_directory 

110 

111 

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. 

115 

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: 

124 

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``. 

129 

130 Returns 

131 ------- 

132 str 

133 Resolved absolute path on success. 

134 A ``FileUtilsError`` message string when validation fails. 

135 

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) 

142 

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 ) 

148 

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) 

155 

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. 

162 

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) 

170 

171 case _: 

172 raise ValueError(f"Invalid file operation option provided: {options!r}") 

173 

174 return path_to_file # reached by all arms that don't return early