Files
open62541/drivers/file_transfer/file_transfer_file.c
T
Andreas Ebner 0503c8fd97 feat(driver): open files with the Session of the caller (Part 20 4.2.2)
The backend open gets the caller as a context: the Session of Open, CreateFile
and the copies of MoveOrCopy, NULL for direct calls. Its result reaches the
client.
2026-10-15 22:05:04 +02:00

537 lines
21 KiB
C

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright 2026 (c) o6 Automation GmbH (Author: Andreas Ebner)
*/
#include "file_transfer_internal.h"
static enum ZIP_CMP
ftHandleOrder(const UA_UInt32 *a, const UA_UInt32 *b) {
return *a < *b ? ZIP_CMP_LESS : (*a > *b ? ZIP_CMP_MORE : ZIP_CMP_EQ);
}
ZIP_FUNCTIONS(FTHandlesById, FTHandle, idTreeEntry, UA_UInt32, handle, ftHandleOrder)
ZIP_FUNCTIONS(FTHandlesBySession, FTHandle, sessionTreeEntry,
UA_NodeId, sessionId, ftNodeIdOrder)
#define UA_FILEACCESS_ALL \
(UA_FILEACCESS_READ | UA_FILEACCESS_WRITE | UA_FILEACCESS_TRAVERSE)
/* A NULL Session requests general permissions only. The callbacks operate on
* the Object, not on its Properties. Unknown permission bits are discarded.
* With vanishedOk, an entry whose storage is gone (a zombie, Bad_NotFound or
* an entry of the other kind at its path) is only restricted by the
* configuration and the callbacks. */
static UA_StatusCode
accessRights(UA_Server *server, const FTEntry *node,
const UA_NodeId *sessionId, void *sessionContext,
UA_Boolean vanishedOk, UA_FileAccessRights *outRights) {
*outRights = 0;
UA_FileTransferFileBackend *b = &node->driver->backend.file;
UA_FileAccessRights rights = UA_FILEACCESS_ALL;
UA_StatusCode res;
if(!vanishedOk || !node->zombie) {
UA_FileTransferFileInfo info;
res = backendGetInfo(b, node->path, &info);
UA_Boolean vanished = vanishedOk &&
(res == UA_STATUSCODE_BADNOTFOUND ||
(res == UA_STATUSCODE_GOOD && info.isDirectory != node->isDirectory));
if(res != UA_STATUSCODE_GOOD && !vanished)
return res;
if(!vanished)
rights = info.accessRights & UA_FILEACCESS_ALL;
}
const FTConfig *opts = &node->driver->config;
if(opts->readOnly)
rights &= (UA_FileAccessRights)~UA_FILEACCESS_WRITE;
if(b->getAccessRights) {
UA_FileAccessRights general = 0;
res = b->getAccessRights(b, server, &node->nodeId, &general);
if(res != UA_STATUSCODE_GOOD)
return res;
rights &= general;
}
if(sessionId && b->getUserAccessRights) {
UA_FileAccessRights user = 0;
res = b->getUserAccessRights(b, server, sessionId, sessionContext,
&node->nodeId, &user);
if(res != UA_STATUSCODE_GOOD)
return res;
rights &= user;
}
*outRights = rights;
return UA_STATUSCODE_GOOD;
}
/* Like Open, the Writable Properties need traversal of the managed ancestors */
UA_StatusCode
getFTAccessRights(UA_Server *server, const FTEntry *node,
const UA_NodeId *sessionId, void *sessionContext,
UA_FileAccessRights *outRights) {
UA_StatusCode res = accessRights(server, node, sessionId, sessionContext,
false, outRights);
for(const FTEntry *parent = node->parent;
res == UA_STATUSCODE_GOOD && parent; parent = parent->parent) {
UA_FileAccessRights rights = 0;
res = accessRights(server, parent, sessionId, sessionContext, false, &rights);
if(res == UA_STATUSCODE_GOOD && !(rights & UA_FILEACCESS_TRAVERSE)) {
*outRights = 0;
break;
}
}
if(res != UA_STATUSCODE_GOOD)
*outRights = 0;
return res;
}
/* Check the Object and traverse permission on its managed ancestor directories.
* A standalone file has no managed ancestors; its backend resolves the path. */
static UA_StatusCode
checkAccess(UA_Server *server, const FTEntry *node,
const UA_NodeId *sessionId, void *sessionContext,
UA_FileAccessRights required, UA_StatusCode deniedStatus,
UA_Boolean vanishedOk) {
UA_FileAccessRights rights = 0;
UA_StatusCode res = accessRights(server, node, sessionId, sessionContext,
vanishedOk, &rights);
if(res != UA_STATUSCODE_GOOD)
return res;
if((rights & required) != required)
return deniedStatus;
for(const FTEntry *parent = node->parent; parent; parent = parent->parent) {
res = accessRights(server, parent, sessionId, sessionContext,
vanishedOk, &rights);
if(res != UA_STATUSCODE_GOOD)
return res;
if(!(rights & UA_FILEACCESS_TRAVERSE))
return UA_STATUSCODE_BADUSERACCESSDENIED;
}
return UA_STATUSCODE_GOOD;
}
UA_StatusCode
checkFTAccess(UA_Server *server, const FTEntry *node,
const UA_NodeId *sessionId, void *sessionContext,
UA_FileAccessRights required,
UA_StatusCode deniedStatus) {
return checkAccess(server, node, sessionId, sessionContext, required,
deniedStatus, false);
}
/* Read and Write recheck the permissions of an open handle. Like an unlinked
* file, an entry that vanished from the storage keeps its open handles, also
* when a parent directory was replaced. Then only the configuration and the
* access callbacks apply. */
static UA_StatusCode
checkHandleAccess(UA_Server *server, const FTHandle *h,
const UA_NodeId *sessionId, void *sessionContext,
UA_FileAccessRights required, UA_StatusCode deniedStatus) {
return checkAccess(server, h->file, sessionId, sessionContext, required,
deniedStatus, true);
}
FTHandle *
findFTHandle(FileTransferDriver *ftd, const UA_NodeId *sessionId,
UA_UInt32 handle) {
FTHandle *h = ZIP_FIND(FTHandlesById, &ftd->handlesById, &handle);
return h && UA_NodeId_equal(&h->sessionId, sessionId) ? h : NULL;
}
static void *
countSessionHandle(void *context, FTHandle *handle) {
(*(size_t*)context)++;
return NULL;
}
static size_t
countSessionHandles(FileTransferDriver *ftd, const UA_NodeId *sessionId) {
size_t count = 0;
ZIP_ITER_KEY(FTHandlesBySession, &ftd->handlesBySession,
sessionId, countSessionHandle, &count);
return count;
}
/* Part 20 requires Session-unique handles. Each driver owns its counter and
* handles; check the other instances without a shared allocator or registry. */
static UA_UInt32
newHandleId(FileTransferDriver *ftd) {
UA_Boolean inUse;
do {
ftd->nextHandle++;
if(ftd->nextHandle == 0)
ftd->nextHandle = 1;
inUse = false;
for(UA_Driver *drv = UA_Server_getDrivers(ftd->driver.server);
drv && !inUse; drv = drv->next) {
if(!isFileTransferDriver(drv))
continue;
inUse = ZIP_FIND(FTHandlesById, &((FileTransferDriver*)drv)->handlesById,
&ftd->nextHandle) != NULL;
}
} while(inUse);
return ftd->nextHandle;
}
/* Close the backend handle and release the handle. Removes zombie nodes once
* their last handle is closed. */
UA_StatusCode
closeFTHandle(UA_Server *server, FTHandle *h) {
FTEntry *node = h->file;
UA_FileTransferFileBackend *b = &node->driver->backend.file;
UA_StatusCode res = b->close(b, h->backendHandle);
ZIP_REMOVE(FTHandlesById, &node->driver->handlesById, h);
ZIP_REMOVE(FTHandlesBySession, &node->driver->handlesBySession, h);
UA_NodeId_clear(&h->sessionId);
if(h->mode & UA_OPENFILEMODE_WRITE)
node->openForWrite = false;
for(FTEntry *entry = node; entry; entry = entry->parent) {
UA_assert(entry->subtreeHandleCount > 0);
entry->subtreeHandleCount--;
}
UA_free(h);
if(node->zombie && node->subtreeHandleCount == 0)
removeSubtree(server, node);
return res;
}
/* The backend may leave fields of the FileInfo unset */
UA_StatusCode
backendGetInfo(UA_FileTransferFileBackend *b, const UA_String path,
UA_FileTransferFileInfo *info) {
memset(info, 0, sizeof(UA_FileTransferFileInfo));
return b->getInfo(b, path, info);
}
/* Close only this instance's handles. */
void
closeDriverHandles(UA_Server *server, FileTransferDriver *ftd) {
FTHandle *h;
while((h = ZIP_ROOT(&ftd->handlesById)))
closeFTHandle(server, h);
}
void
closeSessionHandles(UA_Server *server, FileTransferDriver *ftd,
const UA_NodeId *sessionId) {
FTHandle *h;
/* Removal rebalances both indexes; find again instead of mutating a tree
* during ZIP_ITER_KEY. Only this Session's handles are visited. */
while((h = ZIP_FIND(FTHandlesBySession, &ftd->handlesBySession, sessionId)))
closeFTHandle(server, h);
}
/**************************************
* FileType Method Callbacks
**************************************/
/* Resolve the FTHandle from the fileHandle Method argument. Handles are only
* valid within the Session that opened them and for the file Object the
* Method is called on. */
static UA_StatusCode
resolveHandle(FileTransferDriver *ftd, const UA_NodeId *sessionId,
const UA_NodeId *objectId, const UA_Variant *arg,
FTHandle **outHandle) {
if(!UA_Variant_hasScalarType(arg, &UA_TYPES[UA_TYPES_UINT32]))
return UA_STATUSCODE_BADINVALIDARGUMENT;
UA_UInt32 handleId = *(UA_UInt32*)arg->data;
FTHandle *h = findFTHandle(ftd, sessionId, handleId);
if(!h || !UA_NodeId_equal(&h->file->nodeId, objectId))
return UA_STATUSCODE_BADINVALIDARGUMENT;
*outHandle = h;
return UA_STATUSCODE_GOOD;
}
/* Open a file and register the handle. Shared between the Open Method and
* CreateFile with requestFileOpen. */
UA_StatusCode
openFileHandle(UA_Server *server, FTEntry *node,
const UA_NodeId *sessionId, void *sessionContext, UA_Byte mode,
FTHandle **outHandle) {
FileTransferDriver *ftd = node->driver;
/* Validate the mode bit combination */
if((mode & ~UA_FILETRANSFER_OPENMODE_ALLBITS) ||
!(mode & (UA_OPENFILEMODE_READ | UA_OPENFILEMODE_WRITE)) ||
((mode & UA_OPENFILEMODE_ERASEEXISTING) &&
!(mode & UA_OPENFILEMODE_WRITE)))
return UA_STATUSCODE_BADINVALIDARGUMENT;
/* Locking semantics (Part 20, 4.2.2): a file that is open cannot be
* opened for writing; a file that is open for writing cannot be opened
* for reading */
UA_Boolean writeBit = (mode & UA_OPENFILEMODE_WRITE) != 0;
if(writeBit && node->subtreeHandleCount > 0)
return UA_STATUSCODE_BADNOTWRITABLE;
if((mode & UA_OPENFILEMODE_READ) && node->openForWrite)
return UA_STATUSCODE_BADNOTREADABLE;
UA_StatusCode res = UA_STATUSCODE_GOOD;
if(writeBit)
res = checkFTAccess(server, node, sessionId, sessionContext,
UA_FILEACCESS_WRITE,
UA_STATUSCODE_BADNOTWRITABLE);
if(res == UA_STATUSCODE_GOOD && (mode & UA_OPENFILEMODE_READ))
res = checkFTAccess(server, node, sessionId, sessionContext,
UA_FILEACCESS_READ,
UA_STATUSCODE_BADNOTREADABLE);
if(res != UA_STATUSCODE_GOOD)
return res;
/* Resource limits */
if(node->subtreeHandleCount >= ftd->config.maxHandlesPerFile ||
countSessionHandles(ftd, sessionId) >= ftd->config.maxHandlesPerSession)
return UA_STATUSCODE_BADRESOURCEUNAVAILABLE;
UA_FileTransferFileBackend *b = &node->driver->backend.file;
UA_FileTransferOpenContext context = {server, sessionId, sessionContext};
UA_UInt32 backendHandle = 0;
res = b->open(b, node->path, mode, &context, &backendHandle);
if(res != UA_STATUSCODE_GOOD)
return res;
FTHandle *h = (FTHandle*)UA_calloc(1, sizeof(FTHandle));
if(!h) {
b->close(b, backendHandle);
return UA_STATUSCODE_BADOUTOFMEMORY;
}
res = UA_NodeId_copy(sessionId, &h->sessionId);
if(res != UA_STATUSCODE_GOOD) {
b->close(b, backendHandle);
UA_free(h);
return res;
}
h->handle = newHandleId(ftd);
h->file = node;
h->mode = mode;
h->backendHandle = backendHandle;
ZIP_INSERT(FTHandlesById, &ftd->handlesById, h);
ZIP_INSERT(FTHandlesBySession, &ftd->handlesBySession, h);
for(FTEntry *entry = node; entry; entry = entry->parent)
entry->subtreeHandleCount++;
if(writeBit)
node->openForWrite = true;
*outHandle = h;
return UA_STATUSCODE_GOOD;
}
static UA_StatusCode
openMethodCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *methodId,
void *methodContext, const UA_NodeId *objectId,
void *objectContext, size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output) {
FTEntry *node = resolveFTEntry(server, objectId, objectContext);
if(!node)
return UA_STATUSCODE_BADNOTSUPPORTED;
if(node->isDirectory || node->zombie)
return UA_STATUSCODE_BADNOTFOUND;
UA_StatusCode res;
if(inputSize < 1 || outputSize < 1 ||
!UA_Variant_hasScalarType(&input[0], &UA_TYPES[UA_TYPES_BYTE]))
return UA_STATUSCODE_BADINVALIDARGUMENT;
/* Open takes a Byte bit mask, not the generated 32-bit UA_OpenFileMode. */
UA_Byte mode = *(UA_Byte*)input[0].data;
FTHandle *h = NULL;
res = openFileHandle(server, node, sessionId, sessionContext, mode, &h);
if(res != UA_STATUSCODE_GOOD)
return res;
/* The client cannot close a handle it does not receive */
res = UA_Variant_setScalarCopy(&output[0], &h->handle,
&UA_TYPES[UA_TYPES_UINT32]);
if(res != UA_STATUSCODE_GOOD)
closeFTHandle(server, h);
return res;
}
static UA_StatusCode
closeMethodCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *methodId,
void *methodContext, const UA_NodeId *objectId,
void *objectContext, size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output) {
FTEntry *node = resolveFTEntry(server, objectId, objectContext);
if(!node)
return UA_STATUSCODE_BADNOTSUPPORTED;
FileTransferDriver *ftd = node->driver;
if(inputSize < 1)
return UA_STATUSCODE_BADINVALIDARGUMENT;
FTHandle *h = NULL;
UA_StatusCode res = resolveHandle(ftd, sessionId, objectId, &input[0], &h);
if(res != UA_STATUSCODE_GOOD)
return res;
return closeFTHandle(server, h);
}
static UA_StatusCode
readMethodCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *methodId,
void *methodContext, const UA_NodeId *objectId,
void *objectContext, size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output) {
FTEntry *node = resolveFTEntry(server, objectId, objectContext);
if(!node)
return UA_STATUSCODE_BADNOTSUPPORTED;
FileTransferDriver *ftd = node->driver;
if(inputSize < 2 || outputSize < 1)
return UA_STATUSCODE_BADINVALIDARGUMENT;
FTHandle *h = NULL;
UA_StatusCode res = resolveHandle(ftd, sessionId, objectId, &input[0], &h);
if(res != UA_STATUSCODE_GOOD)
return res;
if(!(h->mode & UA_OPENFILEMODE_READ))
return UA_STATUSCODE_BADINVALIDSTATE;
res = checkHandleAccess(server, h, sessionId, sessionContext,
UA_FILEACCESS_READ, UA_STATUSCODE_BADNOTREADABLE);
if(res != UA_STATUSCODE_GOOD)
return res;
if(!UA_Variant_hasScalarType(&input[1], &UA_TYPES[UA_TYPES_INT32]))
return UA_STATUSCODE_BADINVALIDARGUMENT;
UA_Int32 length = *(UA_Int32*)input[1].data;
if(length <= 0)
return UA_STATUSCODE_BADINVALIDARGUMENT;
/* The Server is allowed to return less data than the requested length */
if((UA_UInt32)length > ftd->config.maxReadLength)
length = (UA_Int32)ftd->config.maxReadLength;
UA_ByteString *data = UA_ByteString_new();
if(!data)
return UA_STATUSCODE_BADOUTOFMEMORY;
UA_FileTransferFileBackend *b = &h->file->driver->backend.file;
res = b->read(b, h->backendHandle, length, data);
if(res != UA_STATUSCODE_GOOD) {
UA_ByteString_delete(data);
return res;
}
/* An empty ByteString, not a null one, marks the end of the file
* (Part 20, 4.2.4) */
if(data->length == 0 && !data->data)
data->data = (UA_Byte*)UA_EMPTY_ARRAY_SENTINEL;
UA_Variant_setScalar(&output[0], data, &UA_TYPES[UA_TYPES_BYTESTRING]);
return UA_STATUSCODE_GOOD;
}
static UA_StatusCode
writeMethodCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *methodId,
void *methodContext, const UA_NodeId *objectId,
void *objectContext, size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output) {
FTEntry *node = resolveFTEntry(server, objectId, objectContext);
if(!node)
return UA_STATUSCODE_BADNOTSUPPORTED;
FileTransferDriver *ftd = node->driver;
if(inputSize < 2)
return UA_STATUSCODE_BADINVALIDARGUMENT;
FTHandle *h = NULL;
UA_StatusCode res = resolveHandle(ftd, sessionId, objectId, &input[0], &h);
if(res != UA_STATUSCODE_GOOD)
return res;
if(!(h->mode & UA_OPENFILEMODE_WRITE))
return UA_STATUSCODE_BADINVALIDSTATE;
res = checkHandleAccess(server, h, sessionId, sessionContext,
UA_FILEACCESS_WRITE, UA_STATUSCODE_BADNOTWRITABLE);
if(res != UA_STATUSCODE_GOOD)
return res;
/* Writing an empty or null ByteString is a no-op with a Good result */
if(UA_Variant_isEmpty(&input[1]))
return UA_STATUSCODE_GOOD;
if(!UA_Variant_hasScalarType(&input[1], &UA_TYPES[UA_TYPES_BYTESTRING]))
return UA_STATUSCODE_BADINVALIDARGUMENT;
UA_ByteString data = *(UA_ByteString*)input[1].data;
if(data.length == 0)
return UA_STATUSCODE_GOOD;
/* The MaxByteStringLength Property announces this limit for Read and Write
* alike (Part 20, 4.2.1). A Read may return less than requested, but
* truncating a Write would silently discard client data, so an oversized
* chunk is rejected instead. */
if(data.length > ftd->config.maxReadLength)
return UA_STATUSCODE_BADINVALIDARGUMENT;
UA_FileTransferFileBackend *b = &h->file->driver->backend.file;
return b->write(b, h->backendHandle, data);
}
static UA_StatusCode
getPositionMethodCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *methodId,
void *methodContext, const UA_NodeId *objectId,
void *objectContext, size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output) {
FTEntry *node = resolveFTEntry(server, objectId, objectContext);
if(!node)
return UA_STATUSCODE_BADNOTSUPPORTED;
FileTransferDriver *ftd = node->driver;
if(inputSize < 1 || outputSize < 1)
return UA_STATUSCODE_BADINVALIDARGUMENT;
FTHandle *h = NULL;
UA_StatusCode res = resolveHandle(ftd, sessionId, objectId, &input[0], &h);
if(res != UA_STATUSCODE_GOOD)
return res;
UA_FileTransferFileBackend *b = &h->file->driver->backend.file;
UA_UInt64 position = 0;
res = b->getPosition(b, h->backendHandle, &position);
if(res != UA_STATUSCODE_GOOD)
return res;
return UA_Variant_setScalarCopy(&output[0], &position,
&UA_TYPES[UA_TYPES_UINT64]);
}
static UA_StatusCode
setPositionMethodCallback(UA_Server *server, const UA_NodeId *sessionId,
void *sessionContext, const UA_NodeId *methodId,
void *methodContext, const UA_NodeId *objectId,
void *objectContext, size_t inputSize, const UA_Variant *input,
size_t outputSize, UA_Variant *output) {
FTEntry *node = resolveFTEntry(server, objectId, objectContext);
if(!node)
return UA_STATUSCODE_BADNOTSUPPORTED;
FileTransferDriver *ftd = node->driver;
if(inputSize < 2)
return UA_STATUSCODE_BADINVALIDARGUMENT;
FTHandle *h = NULL;
UA_StatusCode res = resolveHandle(ftd, sessionId, objectId, &input[0], &h);
if(res != UA_STATUSCODE_GOOD)
return res;
if(!UA_Variant_hasScalarType(&input[1], &UA_TYPES[UA_TYPES_UINT64]))
return UA_STATUSCODE_BADINVALIDARGUMENT;
UA_UInt64 position = *(UA_UInt64*)input[1].data;
UA_FileTransferFileBackend *b = &h->file->driver->backend.file;
return b->setPosition(b, h->backendHandle, position);
}
const FTMethod fileTypeMethods[] = {
{UA_NS0ID_FILETYPE_OPEN, "Open", openMethodCallback},
{UA_NS0ID_FILETYPE_CLOSE, "Close", closeMethodCallback},
{UA_NS0ID_FILETYPE_READ, "Read", readMethodCallback},
{UA_NS0ID_FILETYPE_WRITE, "Write", writeMethodCallback},
{UA_NS0ID_FILETYPE_GETPOSITION, "GetPosition", getPositionMethodCallback},
{UA_NS0ID_FILETYPE_SETPOSITION, "SetPosition", setPositionMethodCallback}
};