/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Text; namespace Opc.Ua.Server.FileSystem { /// /// Encodes / decodes the string-identifier shape used by /// for files, directories, /// and component nodes hung off them. /// /// /// Wire format: /// <rootType>:<providerPath>[?<componentPath>] /// /// rootType is 0 for the mount root, /// 1 for a directory, 2 for a file. /// providerPath is the provider-relative path /// (forward-slash separated). Any literal & or /// ? in the path is escaped with a leading &. /// componentPath is an optional /-separated /// list of SymbolicName values for a child node hung off /// the root (e.g. "Open", "Size"). /// /// internal readonly struct FileSystemNodeId { /// /// Root identifier (mount point). /// public const int Root = 0; /// /// Directory identifier. /// public const int Directory = 1; /// /// File identifier. /// public const int File = 2; public FileSystemNodeId( int rootType, string providerPath, ushort namespaceIndex, string? componentPath = null) { RootType = rootType; ProviderPath = providerPath ?? string.Empty; NamespaceIndex = namespaceIndex; ComponentPath = componentPath; } /// /// 0 = root, 1 = directory, 2 = file. /// public int RootType { get; } /// /// Provider-relative path (empty for the root). /// public string ProviderPath { get; } /// /// Optional component-name chain. /// public string? ComponentPath { get; } /// /// Namespace index of the encoded NodeId. /// public ushort NamespaceIndex { get; } public static NodeId BuildRoot(ushort namespaceIndex) { return new FileSystemNodeId(Root, string.Empty, namespaceIndex).ToNodeId(); } public static NodeId BuildDirectory(string providerPath, ushort namespaceIndex) { return new FileSystemNodeId(Directory, providerPath, namespaceIndex).ToNodeId(); } public static NodeId BuildFile(string providerPath, ushort namespaceIndex) { return new FileSystemNodeId(File, providerPath, namespaceIndex).ToNodeId(); } /// /// Attempts to parse the given into the /// FileSystem wire format. /// public static bool TryParse(NodeId nodeId, out FileSystemNodeId result) { result = default; if (nodeId.IsNull || !nodeId.TryGetValue(out string? identifier) || string.IsNullOrEmpty(identifier)) { return false; } int rootType = 0; int start = -1; for (int ii = 0; ii < identifier.Length; ii++) { if (!char.IsDigit(identifier[ii])) { start = ii; break; } rootType = (rootType * 10) + (identifier[ii] - '0'); } if (start < 0 || start >= identifier.Length || identifier[start] != ':') { return false; } // Read provider path with & escapes; an unescaped ? // terminates and starts the component path. var buffer = new StringBuilder(); int index = start + 1; int end = identifier.Length; bool escaped = false; while (index < end) { char ch = identifier[index++]; if (!escaped && ch == '&') { escaped = true; continue; } if (!escaped && ch == '?') { end = index; break; } buffer.Append(ch); escaped = false; } string? componentPath = end < identifier.Length ? identifier[end..] : null; result = new FileSystemNodeId( rootType, buffer.ToString(), nodeId.NamespaceIndex, componentPath); return true; } /// /// Returns this struct as a with the /// existing root + component path. /// public NodeId ToNodeId() { return ToNodeId(componentName: null); } /// /// Returns this struct as a , optionally /// appending an additional child component name to the /// existing . /// public NodeId ToNodeId(string? componentName) { StringBuilder buffer = new StringBuilder() .Append(RootType).Append(':'); for (int ii = 0; ii < ProviderPath.Length; ii++) { char ch = ProviderPath[ii]; if (ch is '&' or '?') { buffer.Append('&'); } buffer.Append(ch); } if (!string.IsNullOrEmpty(ComponentPath)) { buffer.Append('?').Append(ComponentPath); } if (!string.IsNullOrEmpty(componentName)) { if (string.IsNullOrEmpty(ComponentPath)) { buffer.Append('?'); } else { buffer.Append('/'); } buffer.Append(componentName); } return new NodeId(buffer.ToString(), NamespaceIndex); } /// /// Encodes a NodeId for an arbitrary child component of the /// supplied parent node. Mirrors the old /// ModelUtils.ConstructIdForComponent helper. /// public static NodeId ConstructIdForComponent( NodeState component, ushort namespaceIndex) { if (component == null) { return NodeId.Null; } if (component is not BaseInstanceState instance || instance.Parent == null || !instance.Parent.NodeId.TryGetValue(out string? parentId)) { return component.NodeId; } StringBuilder buffer = new StringBuilder() .Append(parentId) .Append(parentId.IndexOf('?', StringComparison.Ordinal) < 0 ? '?' : '/') .Append(component.SymbolicName); return new NodeId(buffer.ToString(), namespaceIndex); } } }