/** * @module node-opcua-file-transfer */ import { type Byte, coerceInt32, type Int32, type Int64, type UInt16, type UInt32, type UInt64 } from "node-opcua-basic-types"; import { MethodIds } from "node-opcua-constants"; import { AttributeIds, type QualifiedName } from "node-opcua-data-model"; import { checkDebugFlag, make_debugLog } from "node-opcua-debug"; import { type NodeId, resolveNodeId } from "node-opcua-nodeid"; import type { IBasicSessionAsync } from "node-opcua-pseudo-session"; import type { ReadValueIdOptions } from "node-opcua-service-read"; import { type BrowsePath, makeBrowsePath } from "node-opcua-service-translate-browse-path"; import { DataType, VariantArrayType } from "node-opcua-variant"; const debugLog = make_debugLog("FileType"); const doDebug = checkDebugFlag("FileType"); import { OpenFileMode } from "../open_mode.js"; export { OpenFileMode } from "../open_mode.js"; export interface IClientFile { fileHandle: number; open(mode: OpenFileMode): Promise; close(): Promise; getPosition(): Promise; setPosition(position: UInt64 | UInt32): Promise; read(bytesToRead: UInt32 | Int32 | Int64 | UInt64): Promise; write(data: Buffer): Promise; openCount(): Promise; size(): Promise; session: IBasicSessionAsync; } export interface IClientFilePriv extends IClientFile { readonly fileNodeId: NodeId; openMethodNodeId?: NodeId; closeMethodNodeId?: NodeId; setPositionNodeId?: NodeId; getPositionNodeId?: NodeId; readNodeId?: NodeId; writeNodeId?: NodeId; openCountNodeId?: NodeId; sizeNodeId?: NodeId; ensureInitialized(): Promise; } /** * * */ export class ClientFile implements IClientFile { public static useGlobalMethod = false; public fileHandle = 0; public session: IBasicSessionAsync; public readonly nodeId: NodeId; private openMethodNodeId?: NodeId; private closeMethodNodeId?: NodeId; private setPositionNodeId?: NodeId; private getPositionNodeId?: NodeId; private readNodeId?: NodeId; private writeNodeId?: NodeId; private openCountNodeId?: NodeId; private sizeNodeId?: NodeId; constructor(session: IBasicSessionAsync, nodeId: NodeId) { this.session = session; this.nodeId = nodeId; } /** * @deprecated use nodeId instead */ protected get fileNodeId(): NodeId { return this.nodeId; } public async browseName(): Promise { const dataValue = await this.session.read({ nodeId: this.nodeId, attributeId: AttributeIds.BrowseName }); return dataValue.value.value as QualifiedName; } public async open(mode: OpenFileMode): Promise { if (mode === null || mode === undefined) { throw new Error(`expecting a validMode ${OpenFileMode[mode]}`); } if (this.fileHandle) { throw new Error("File has already be opened"); } await this.ensureInitialized(); const result = await this.session.call({ inputArguments: [{ dataType: DataType.Byte, value: mode as Byte }], methodId: this.openMethodNodeId, objectId: this.nodeId }); if (result.statusCode.isNotGood()) { // c8 ignore next doDebug && debugLog("Cannot open file : "); throw new Error(`cannot open file statusCode = ${result.statusCode.toString()} mode = ${OpenFileMode[mode]}`); } this.fileHandle = result.outputArguments?.[0].value; return this.fileHandle; } public async close(): Promise { if (!this.fileHandle) { throw new Error("File has not been opened yet"); } await this.ensureInitialized(); const result = await this.session.call({ inputArguments: [{ dataType: DataType.UInt32, value: this.fileHandle }], methodId: this.closeMethodNodeId, objectId: this.nodeId }); if (result.statusCode.isNotGood()) { // c8 ignore next doDebug && debugLog("Cannot close file : "); throw new Error(`cannot close file statusCode = ${result.statusCode.toString()}`); } this.fileHandle = 0; } public async getPosition(): Promise { await this.ensureInitialized(); if (!this.fileHandle) { throw new Error("File has not been opened yet"); } const result = await this.session.call({ inputArguments: [{ dataType: DataType.UInt32, value: this.fileHandle }], methodId: this.getPositionNodeId, objectId: this.nodeId }); if (result.statusCode.isNotGood()) { throw new Error(`Error ${result.statusCode.toString()}`); } return result.outputArguments?.[0].value as UInt64; } public async setPosition(position: UInt64 | UInt32): Promise { await this.ensureInitialized(); if (!this.fileHandle) { throw new Error("File has not been opened yet"); } if (typeof position === "number") { position = [0, position]; } const result = await this.session.call({ inputArguments: [ { dataType: DataType.UInt32, value: this.fileHandle }, { arrayType: VariantArrayType.Scalar, dataType: DataType.UInt64, value: position } ], methodId: this.setPositionNodeId, objectId: this.nodeId }); if (result.statusCode.isNotGood()) { throw new Error(`Error ${result.statusCode.toString()}`); } return; } public async read(bytesToRead: UInt32 | Int32 | Int64 | UInt64): Promise { await this.ensureInitialized(); if (!this.fileHandle) { throw new Error("File has not been opened yet"); } const result = await this.session.call({ inputArguments: [ { dataType: DataType.UInt32, value: this.fileHandle }, { arrayType: VariantArrayType.Scalar, dataType: DataType.Int32, value: coerceInt32(bytesToRead) } ], methodId: this.readNodeId, objectId: this.nodeId }); if (result.statusCode.isNotGood()) { throw new Error(`Error ${result.statusCode.toString()}`); } if (!result.outputArguments || result.outputArguments[0].dataType !== DataType.ByteString) { throw new Error("Error invalid output"); } return result.outputArguments?.[0].value as Buffer; } public async write(data: Buffer): Promise { await this.ensureInitialized(); if (!this.fileHandle) { throw new Error("File has not been opened yet"); } const result = await this.session.call({ inputArguments: [ { dataType: DataType.UInt32, value: this.fileHandle }, { arrayType: VariantArrayType.Scalar, dataType: DataType.ByteString, value: data } ], methodId: this.writeNodeId, objectId: this.nodeId }); if (result.statusCode.isNotGood()) { throw new Error(`Error ${result.statusCode.toString()}`); } return; } public async openCount(): Promise { await this.ensureInitialized(); const nodeToRead: ReadValueIdOptions = { nodeId: this.openCountNodeId, attributeId: AttributeIds.Value }; const dataValue = await this.session.read(nodeToRead); // c8 ignore next if (doDebug) { debugLog(" OpenCount ", nodeToRead.nodeId?.toString(), dataValue.toString()); } return dataValue.value.value; } public async size(): Promise { await this.ensureInitialized(); const nodeToRead = { nodeId: this.sizeNodeId, attributeId: AttributeIds.Value }; const dataValue = await this.session.read(nodeToRead); return dataValue.value.value; } protected async extractMethodsIds(): Promise { if (ClientFile.useGlobalMethod) { // c8 ignore next doDebug && debugLog("Using GlobalMethodId"); this.openMethodNodeId = resolveNodeId(MethodIds.FileType_Open); this.closeMethodNodeId = resolveNodeId(MethodIds.FileType_Close); this.setPositionNodeId = resolveNodeId(MethodIds.FileType_SetPosition); this.getPositionNodeId = resolveNodeId(MethodIds.FileType_GetPosition); this.writeNodeId = resolveNodeId(MethodIds.FileType_Write); this.readNodeId = resolveNodeId(MethodIds.FileType_Read); const browsePaths: BrowsePath[] = [makeBrowsePath(this.nodeId, "/OpenCount"), makeBrowsePath(this.nodeId, "/Size")]; const results = await this.session.translateBrowsePath(browsePaths); if (results[0].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory OpenCount Property"); } if (results[1].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory Size Property"); } this.openCountNodeId = results[0].targets?.[0].targetId; this.sizeNodeId = results[1].targets?.[0].targetId; return; } const browsePaths: BrowsePath[] = [ makeBrowsePath(this.nodeId, "/Open"), makeBrowsePath(this.nodeId, "/Close"), makeBrowsePath(this.nodeId, "/SetPosition"), makeBrowsePath(this.nodeId, "/GetPosition"), makeBrowsePath(this.nodeId, "/Write"), makeBrowsePath(this.nodeId, "/Read"), makeBrowsePath(this.nodeId, "/OpenCount"), makeBrowsePath(this.nodeId, "/Size") ]; const results = await this.session.translateBrowsePath(browsePaths); if (results[0].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory Open Method"); } if (results[1].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory Close Method"); } if (results[2].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory SetPosition Method"); } if (results[3].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory GetPosition Method"); } if (results[4].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory Write Method"); } if (results[5].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory Read Method"); } if (results[6].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory OpenCount Variable"); } if (results[7].statusCode.isNotGood()) { throw new Error("fileType object does not expose mandatory Size Variable"); } // biome-ignore lint/correctness/noConstantCondition: deliberate debug on/off toggle, not dead code if (false && doDebug) { results.map((x) => debugLog(x.toString())); } this.openMethodNodeId = results[0].targets?.[0].targetId; this.closeMethodNodeId = results[1].targets?.[0].targetId; this.setPositionNodeId = results[2].targets?.[0].targetId; this.getPositionNodeId = results[3].targets?.[0].targetId; this.writeNodeId = results[4].targets?.[0].targetId; this.readNodeId = results[5].targets?.[0].targetId; this.openCountNodeId = results[6].targets?.[0].targetId; this.sizeNodeId = results[7].targets?.[0].targetId; } protected async ensureInitialized(): Promise { if (!this.openMethodNodeId) { await this.extractMethodsIds(); } } } /** * 5.2.10 UserRolePermissions * * The optional UserRolePermissions Attribute specifies the Permissions that apply to a Node for * all Roles granted to current Session. The value of the Attribute is an array of * RolePermissionType Structures (see Table 8). * Clients may determine their effective Permissions by logically ORing the Permissions for each * Role in the array. * The value of this Attribute is derived from the rules used by the Server to map Sessions to * Roles. This mapping may be vendor specific or it may use the standard Role model defined in 4.8. * This Attribute shall not be writeable. * If not specified, the value of DefaultUserRolePermissions Property from the Namespace * Metadata Object associated with the Node is used instead. If the NamespaceMetadata Object * does not define the Property or does not exist, then the Server does not publish any information * about Roles mapped to the current Session. * * * 5.2.11 AccessRestrictions * The optional AccessRestrictions Attribute specifies the AccessRestrictions that apply to a Node. * Its data type is defined in 8.56. If a Server supports AccessRestrictions for a particular * Namespace it adds the DefaultAccessRestrictions Property to the NamespaceMetadata Object * for that Namespace (see Figure 8). If a particular Node in the Namespace needs to override * the default value the Server adds the AccessRestrictions Attribute to the Node. * If a Server implements a vendor specific access restriction model for a Namespace, it does not * add the DefaultAccessRestrictions Property to the NamespaceMetadata Object. * * * DefaultAccessRestrictions * */