diff --git a/backend/src/packages/chaiNNer_standard/utility/directory/directory_go_into.py b/backend/src/packages/chaiNNer_standard/utility/directory/directory_go_into.py new file mode 100644 index 000000000..4a3dcfcb0 --- /dev/null +++ b/backend/src/packages/chaiNNer_standard/utility/directory/directory_go_into.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import re +from pathlib import Path + +from nodes.groups import optional_list_group +from nodes.properties.inputs import DirectoryInput, TextInput +from nodes.properties.outputs import DirectoryOutput + +from .. import directory_group + +INVALID_CHARS = re.compile(r"[<>:\"|?*\x00-\x1F]") + + +def is_abs(path: str) -> bool: + return path.startswith(("/", "\\")) or Path(path).is_absolute() + + +def go_into(dir: Path, folder: str) -> Path: + if is_abs(folder): + raise ValueError("Absolute paths are not allowed as folders.") + + invalid = INVALID_CHARS.search(folder) + if invalid is not None: + raise ValueError(f"Invalid character '{invalid.group()}' in folder name.") + + return (dir / folder).resolve() + + +@directory_group.register( + schema_id="chainner:utility:into_directory", + name="Directory Go Into", + description="Goes forward into a directory.", + icon="BsFolder", + inputs=[ + DirectoryInput(must_exist=False, label_style="hidden"), + TextInput("Folder"), + optional_list_group( + *[TextInput(f"Folder {i}").make_optional() for i in range(2, 11)], + ), + ], + outputs=[ + DirectoryOutput( + output_type=""" + def into(dir: Directory | Error, folder: string | null): Directory | Error { + match dir { + Error as e => e, + Directory => { + match folder { + null => dir, + string => { + let result = goIntoDirectory(dir.path, folder); + match result { + string => Directory { path: result }, + Error => result, + } + }, + } + }, + } + } + + let d1 = into(Input0, Input1); + let d2 = into(d1, Input2); + let d3 = into(d2, Input3); + let d4 = into(d3, Input4); + let d5 = into(d4, Input5); + let d6 = into(d5, Input6); + let d7 = into(d6, Input7); + let d8 = into(d7, Input8); + let d9 = into(d8, Input9); + let d10 = into(d9, Input10); + d10 + """, + ), + ], +) +def directory_go_into_node(directory: Path, *folders: str | None) -> Path: + for folder in folders: + if folder is not None: + directory = go_into(directory, folder) + return directory diff --git a/src/common/types/chainner-builtin.ts b/src/common/types/chainner-builtin.ts index 32c3ef67a..641f9390c 100644 --- a/src/common/types/chainner-builtin.ts +++ b/src/common/types/chainner-builtin.ts @@ -17,6 +17,7 @@ import { union, wrapBinary, wrapQuaternary, + wrapScopedBinary, wrapScopedUnary, wrapTernary, } from '@chainner/navi'; @@ -412,3 +413,47 @@ export const getParentDirectory = wrapBinary:"|?*\x00-\x1F]/; +const goIntoDirectoryImpl = (basePath: string, relPath: string): string | Error => { + const isAbsolute = /^[/\\]/.test(relPath) || path.isAbsolute(relPath); + if (isAbsolute) { + return new Error('Absolute paths are not allowed as folders.'); + } + + const invalid = INVALID_PATH_CHARS.exec(relPath); + if (invalid) { + return new Error(`Invalid character '${invalid[0]}' in folder name.`); + } + + const joined = path.join(basePath, relPath); + return path.resolve(joined); +}; +export const goIntoDirectory = wrapScopedBinary( + ( + scope, + basePath: StringPrimitive, + relPath: StringPrimitive + ): Arg => { + const errorDesc = getStructDescriptor(scope, 'Error'); + + if (basePath.type === 'literal' && relPath.type === 'literal') { + try { + const result = goIntoDirectoryImpl(basePath.value, relPath.value); + if (typeof result === 'string') { + return literal(result); + } + return createInstance(errorDesc, { + message: literal(result.message), + }); + } catch (e) { + return createInstance(errorDesc, { + message: literal(String(e)), + }); + } + } + + return union(StringType.instance, errorDesc.default); + } +); diff --git a/src/common/types/chainner-scope.ts b/src/common/types/chainner-scope.ts index 33fe2da7e..2b7ee55b7 100644 --- a/src/common/types/chainner-scope.ts +++ b/src/common/types/chainner-scope.ts @@ -13,6 +13,7 @@ import { lazy } from '../util'; import { formatTextPattern, getParentDirectory, + goIntoDirectory, padCenter, padEnd, padStart, @@ -128,6 +129,7 @@ intrinsic def padCenter(text: string, width: uint, padding: string): string; intrinsic def splitFilePath(path: string): SplitFilePath; intrinsic def parseColorJson(json: string): Color; intrinsic def getParentDirectory(path: string, times: uint): string; +intrinsic def goIntoDirectory(basePath: string, path: string): string | Error; `; export const getChainnerScope = lazy((): Scope => { @@ -142,6 +144,7 @@ export const getChainnerScope = lazy((): Scope => { splitFilePath, parseColorJson, getParentDirectory: makeScoped(getParentDirectory), + goIntoDirectory, }; const definitions = parseDefinitions(new SourceDocument(code, 'chainner-internal'));