using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// /// The Files script global (design 2026-09-28): READ-ONLY access to files under the site /// node's configured ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots — e.g. a /// recipe file whose path a MES/Delmia call handed to the machine. Off by default. Every /// refusal or failure throws ; reads are bounded by the /// script's own execution deadline as well as the caller's token. /// /// foreach (var line in await Files.ReadLinesAsync(path)) { … } /// /// public sealed class FilesAccessor { private readonly IScriptFileReader? _reader; private readonly CancellationToken _scriptToken; /// Initializes a new instance. /// The host reader; null when the node has none wired (then every call throws Disabled). /// The running script's execution-timeout token. public FilesAccessor(IScriptFileReader? reader, CancellationToken scriptToken) { _reader = reader; _scriptToken = scriptToken; } /// Reads a whole text file (BOM-detected, UTF-8 by default). /// Absolute path under an allowed root. /// Cancels the read. /// The file's text. public async Task ReadTextAsync(string path, CancellationToken cancellationToken = default) { using var linked = CancellationTokenSource.CreateLinkedTokenSource(_scriptToken, cancellationToken); return await Reader.ReadTextAsync(path, linked.Token).ConfigureAwait(false); } /// /// Reads a text file as lines (\r\n, \n or \r); a trailing line break does /// not produce a final empty line. /// /// Absolute path under an allowed root. /// Cancels the read. /// The file's lines. public async Task> ReadLinesAsync(string path, CancellationToken cancellationToken = default) { var text = await ReadTextAsync(path, cancellationToken).ConfigureAwait(false); return SplitLines(text); } /// True when an existing file is at the path; false when nothing is there. Still refuses paths outside the allowed roots. /// Absolute path under an allowed root. /// Cancels the check. /// Whether the file exists. public async Task ExistsAsync(string path, CancellationToken cancellationToken = default) { using var linked = CancellationTokenSource.CreateLinkedTokenSource(_scriptToken, cancellationToken); return await Reader.ExistsAsync(path, linked.Token).ConfigureAwait(false); } private IScriptFileReader Reader => _reader ?? throw new ScriptFileAccessException(ScriptFileAccessFailure.Disabled, "Script file access is not available on this node."); internal static IReadOnlyList SplitLines(string text) { if (text.Length == 0) return Array.Empty(); var lines = text.Split(["\r\n", "\n", "\r"], StringSplitOptions.None).ToList(); if (lines.Count > 0 && lines[^1].Length == 0) lines.RemoveAt(lines.Count - 1); return lines; } }