- diff --git a/build.vc19/nfsd/nfsd.vcxproj b/build.vc19/nfsd/nfsd.vcxproj
- index 8589a2a..face630 100644
- --- a/build.vc19/nfsd/nfsd.vcxproj
- +++ b/build.vc19/nfsd/nfsd.vcxproj
- @@ -331,6 +331,7 @@
- <ClCompile Include="..\..\daemon\upcall.c" />
- <ClCompile Include="..\..\daemon\util.c" />
- <ClCompile Include="..\..\daemon\volume.c" />
- + <ClCompile Include="..\..\daemon\winstreams.c" />
- </ItemGroup>
- <ItemGroup>
- <ClInclude Include="..\..\daemon\accesstoken.h" />
- @@ -356,6 +357,7 @@
- <ClInclude Include="..\..\daemon\tree.h" />
- <ClInclude Include="..\..\daemon\upcall.h" />
- <ClInclude Include="..\..\daemon\util.h" />
- + <ClInclude Include="..\..\daemon\winstreams.h" />
- <ClInclude Include="..\..\include\from_kernel.h" />
- <ClInclude Include="..\..\include\nfs_ea.h" />
- </ItemGroup>
- diff --git a/build.vc19/nfsd/nfsd.vcxproj.filters b/build.vc19/nfsd/nfsd.vcxproj.filters
- index 134404f..8e7c552 100644
- --- a/build.vc19/nfsd/nfsd.vcxproj.filters
- +++ b/build.vc19/nfsd/nfsd.vcxproj.filters
- @@ -147,6 +147,9 @@
- <ClCompile Include="..\..\daemon\volume.c">
- <Filter>Source Files</Filter>
- </ClCompile>
- + <ClCompile Include="..\..\daemon\winstreams.c">
- + <Filter>Source Files</Filter>
- + </ClCompile>
- <ClCompile Include="..\..\daemon\accesstoken.c">
- <Filter>Source Files</Filter>
- </ClCompile>
- @@ -227,5 +230,8 @@
- <ClInclude Include="..\..\daemon\accesstoken.h">
- <Filter>Header Files</Filter>
- </ClInclude>
- + <ClInclude Include="..\..\daemon\winstreams.h">
- + <Filter>Header Files</Filter>
- + </ClInclude>
- </ItemGroup>
- </Project>
- \ No newline at end of file
- diff --git a/daemon/fileinfoutil.c b/daemon/fileinfoutil.c
- index 0ed40c8..d745fd2 100644
- --- a/daemon/fileinfoutil.c
- +++ b/daemon/fileinfoutil.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -59,7 +59,7 @@ ULONG nfs_file_info_to_attributes(
- if (info->symlink_dir)
- attrs |= FILE_ATTRIBUTE_DIRECTORY;
- }
- - else if (info->type == NF4REG) {
- + else if ((info->type == NF4REG) || (info->type == NF4NAMEDATTR)) {
- if (superblock->sparse_file_support) {
- /* FIXME: What about pNFS ? */
- attrs |= FILE_ATTRIBUTE_SPARSE_FILE;
- @@ -383,6 +383,7 @@ void nfs_to_stat_lx_info(
- stat_lx_out->LxMode = 0UL;
- switch(info->type) {
- case NF4REG:
- + case NF4NAMEDATTR:
- stat_lx_out->LxMode |= LX_MODE_S_IFREG;
- break;
- case NF4DIR:
- diff --git a/daemon/getattr.c b/daemon/getattr.c
- index 4744285..63b985e 100644
- --- a/daemon/getattr.c
- +++ b/daemon/getattr.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -32,6 +32,9 @@
- #include "upcall.h"
- #include "fileinfoutil.h"
- #include "daemon_debug.h"
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- +#include "winstreams.h"
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- int nfs41_cached_getattr(
- @@ -204,6 +207,31 @@ static int handle_getattr(void *daemon_context, nfs41_upcall *upcall)
- &args->stat_lx_info);
- break;
- #endif /* NFS41_DRIVER_WSL_SUPPORT */
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + case FileStreamInformation:
- + args->stream_info_list = NULL;
- +
- + nfs41_path_fh parent = { 0 };
- +
- + status = nfs41_rpc_openattr(state->session, &state->file, FALSE, &parent.fh);
- + /* No named attribute directory ? */
- + if (status == NFS4ERR_NOENT) {
- + DPRINTF(0, ("FileStreamInformation: no named attribute directory for '%s'\n", state->file.name.name));
- + /* FIXME: We should return a default "file::$DATA" entry */
- + } else if (status) {
- + eprintf("FileStreamInformation: "
- + "nfs41_rpc_openattr() failed with '%s'\n",
- + nfs_error_string(status));
- + status = nfs_to_windows_error(status, ERROR_BAD_NET_RESP);
- + goto out;
- + }
- +
- + status = get_stream_list(state,
- + &parent,
- + &args->stream_info_list,
- + &args->stream_info_list_size);
- + break;
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- default:
- eprintf("handle_getattr(state->path.path='%s'): "
- "unhandled file query class %d\n",
- @@ -222,7 +250,7 @@ static int marshall_getattr(
- nfs41_upcall *restrict upcall)
- {
- int status;
- - const getattr_upcall_args *args = &upcall->args.getattr;
- + getattr_upcall_args *args = &upcall->args.getattr;
- uint32_t info_len;
- switch (args->query_class) {
- @@ -292,6 +320,15 @@ static int marshall_getattr(
- if (status) goto out;
- break;
- #endif /* NFS41_DRIVER_WSL_SUPPORT */
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + case FileStreamInformation:
- + info_len = args->stream_info_list_size;
- + status = safe_write(&buffer, length, &info_len, sizeof(info_len));
- + if (status) goto out;
- + status = safe_write(&buffer, length, args->stream_info_list, info_len);
- + if (status) goto out;
- + break;
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- default:
- eprintf("marshall_getattr: unknown file query class %d\n",
- args->query_class);
- @@ -302,6 +339,13 @@ static int marshall_getattr(
- if (status) goto out;
- DPRINTF(1, ("NFS41_SYSOP_FILE_QUERY: downcall changattr=%llu\n", args->ctime));
- out:
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (args->query_class == FileStreamInformation) {
- + free(args->stream_info_list);
- + args->stream_info_list = NULL;
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- return status;
- }
- diff --git a/daemon/lookup.c b/daemon/lookup.c
- index 3e9e591..39074e2 100644
- --- a/daemon/lookup.c
- +++ b/daemon/lookup.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -25,12 +25,16 @@
- #include <strsafe.h>
- #include <time.h>
- +#include "nfs41_build_features.h"
- #include "nfs41_compound.h"
- #include "nfs41_ops.h"
- #include "name_cache.h"
- #include "fileinfoutil.h"
- #include "util.h"
- #include "daemon_debug.h"
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- +#include "winstreams.h"
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- #define LULVL 2 /* dprintf level for lookup logging */
- @@ -502,6 +506,156 @@ int nfs41_lookup(
- DPRINTF(LULVL, ("--> nfs41_lookup('%s')\n", path.path));
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + /* Fast check for potential stream: Look for a colon */
- + if (is_stream_path(path_inout)) {
- + char base_name[NFS41_MAX_PATH_LEN+1];
- + char stream_name[NFS41_MAX_COMPONENT_LEN+1];
- + bool is_stream = false;
- +
- + /* Parse the stream syntax */
- + status = parse_win32stream_name(path.path, &is_stream, base_name, stream_name);
- + if (status)
- + goto out;
- +
- + if (is_stream) {
- + if (stream_name[0] == '\0') {
- + /* "foo::$DATA" case -> treat as "foo" */
- + DPRINTF(2, ("nfs41_lookup: 'foo::$DATA' case -> treat as 'foo'\n"));
- + size_t base_len = strlen(base_name);
- +
- + (void)memcpy(path.path, base_name, base_len);
- + path.path[base_len] = '\0';
- + path.len = (unsigned short)base_len;
- + path_pos = path.path;
- + path_end = path.path + path.len;
- +
- + /* Continue to normal lookup with stripped name */
- + } else {
- + /* "foo:bar" case */
- + DPRINTF(2, ("nfs41_lookup: base_name='%s', stream_name='%s'\n",
- + base_name, stream_name));
- +
- + /*
- + * Recursively lookup the base file ...
- + */
- + nfs41_abs_path base_path = path;
- + size_t base_len = strlen(base_name);
- +
- + (void)memcpy(base_path.path, base_name, base_len);
- + base_path.path[base_len] = '\0';
- + base_path.len = (unsigned short)base_len;
- +
- + nfs41_path_fh attr_target;
- + if (target_out == NULL) {
- + target_out = &attr_target;
- + }
- +
- + status = nfs41_lookup(root, session, casesensitive, &base_path,
- + parent_out, target_out, info_out, session_out);
- + if (status)
- + goto out;
- +
- + /*
- + * ... and then lookup the NFSv4 named attribute
- + */
- + nfs41_session *lookup_session;
- + nfs41_compound compound;
- + nfs_argop4 argops[8];
- + nfs_resop4 resops[8];
- + nfs41_sequence_args sequence_args;
- + nfs41_sequence_res sequence_res;
- + nfs41_openattr_args openattr_args;
- + nfs41_openattr_res openattr_res;
- + nfs41_putfh_args putfh_args = { 0 };
- + nfs41_putfh_res putfh_res;
- + nfs41_lookup_args lookup_args = { 0 };
- + nfs41_lookup_res lookup_res;
- + nfs41_getfh_res getfh_res;
- + nfs41_getattr_args getattr_args = { 0 };
- + nfs41_getattr_res getattr_res = { 0 };
- + nfs41_component stream_comp = {
- + .name = stream_name,
- + .len = (unsigned short)strlen(stream_name)
- + };
- + /* Minimal attributes for stream */
- + bitmap4 attr_request = {
- + .count = 2,
- + .arr = {
- + [0] = FATTR4_WORD0_TYPE | FATTR4_WORD0_CHANGE |
- + FATTR4_WORD0_SIZE | FATTR4_WORD0_FSID |
- + FATTR4_WORD0_FILEID,
- + [1] = FATTR4_WORD1_MODE | FATTR4_WORD1_SPACE_USED |
- + FATTR4_WORD1_TIME_ACCESS |
- + FATTR4_WORD1_TIME_CREATE |
- + FATTR4_WORD1_TIME_MODIFY |
- + FATTR4_WORD1_OWNER | FATTR4_WORD1_OWNER_GROUP,
- + [2] = 0
- + }
- + };
- +
- + DPRINTF(1,
- + ("nfs41_lookup: "
- + "Looking up base_name='%s'/stream_name='%s'\n",
- + base_name, stream_name));
- +
- + /* Use the session returned by the base lookup if applicable */
- + if ((session_out != NULL) && (*session_out != NULL))
- + lookup_session = *session_out;
- + else
- + lookup_session = session;
- +
- + compound_init(&compound,
- + lookup_session->client->root->nfsminorvers,
- + argops, resops, "lookup_win32stream");
- +
- + compound_add_op(&compound, OP_SEQUENCE, &sequence_args, &sequence_res);
- + nfs41_session_sequence(&sequence_args, lookup_session, 0);
- +
- + compound_add_op(&compound, OP_PUTFH, &putfh_args, &putfh_res);
- + putfh_args.file = target_out;
- + putfh_args.in_recovery = FALSE;
- +
- + compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
- + openattr_args.createdir = FALSE;
- +
- + compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
- + lookup_args.name = &stream_comp;
- +
- + compound_add_op(&compound, OP_GETFH, NULL, &getfh_res);
- + getfh_res.fh = &target_out->fh;
- +
- + compound_add_op(&compound, OP_GETATTR, &getattr_args, &getattr_res);
- + getattr_args.attr_request = &attr_request;
- + /* If caller wanted info, fill it; else dummy */
- + nfs41_file_info dummy_info;
- + if (info_out)
- + getattr_res.info = info_out;
- + else
- + getattr_res.info = &dummy_info;
- + getattr_res.obj_attributes.attr_vals_len = NFS4_OPAQUE_LIMIT_ATTR;
- +
- + status = compound_encode_send_decode(lookup_session, &compound, TRUE);
- +
- + if (status == 0)
- + status = compound.res.status;
- +
- + if (status == 0) {
- + /* We MUST return a |NF4NAMEDATTR|, and never a |NF4ATTRDIR| */
- + EASSERT(getattr_res.info->type != NF4ATTRDIR);
- + EASSERT(getattr_res.info->type == NF4NAMEDATTR);
- + }
- + else {
- + DPRINTF(1, ("nfs41_lookup: failed for attr, status=%d\n",
- + (int)status));
- + }
- +
- + goto out;
- + }
- + }
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- if (parent_out == NULL) parent_out = &parent;
- if (target_out == NULL) target_out = ⌖
- parent_out->fh.len = target_out->fh.len = 0;
- diff --git a/daemon/nfs41_ops.c b/daemon/nfs41_ops.c
- index 9012db4..6a1a395 100644
- --- a/daemon/nfs41_ops.c
- +++ b/daemon/nfs41_ops.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2024-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2024-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -36,6 +36,9 @@
- #include "delegation.h"
- #include "daemon_debug.h"
- #include "util.h"
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- +#include "winstreams.h"
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- #ifdef NFS41_DRIVER_STABILITY_HACKS
- /*
- @@ -453,8 +456,8 @@ int nfs41_open(
- {
- int status;
- nfs41_compound compound;
- - nfs_argop4 argops[8];
- - nfs_resop4 resops[8];
- + nfs_argop4 argops[12];
- + nfs_resop4 resops[12];
- nfs41_sequence_args sequence_args;
- nfs41_sequence_res sequence_res;
- nfs41_putfh_args putfh_args[2];
- @@ -471,10 +474,39 @@ int nfs41_open(
- bool_t current_fh_is_dir;
- bool_t already_delegated = delegation->type == OPEN_DELEGATE_READ
- || delegation->type == OPEN_DELEGATE_WRITE;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + char base_buf[NFS41_MAX_PATH_LEN+1];
- + char stream_buf[NFS41_MAX_COMPONENT_LEN+1];
- + bool is_stream = false;
- + nfs41_component base_comp = {0};
- + nfs41_component stream_comp = {0};
- + nfs41_lookup_args lookup_args;
- + nfs41_lookup_res lookup_res;
- + nfs41_openattr_args openattr_args;
- + nfs41_openattr_res openattr_res;
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- EASSERT_IS_VALID_NON_NULL_PTR(parent);
- EASSERT_IS_VALID_NON_NULL_PTR(parent->fh.superblock);
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (file->name.name) {
- + if (is_stream_path_fh(file)) {
- + status = parse_win32stream_name(file->name.name,
- + &is_stream, base_buf, stream_buf);
- + if (status)
- + goto out;
- +
- + if (is_stream) {
- + base_comp.name = base_buf;
- + base_comp.len = (unsigned short)strlen(base_buf);
- + stream_comp.name = stream_buf;
- + stream_comp.len = (unsigned short)strlen(stream_buf);
- + }
- + }
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- /* depending on the claim type, OPEN expects CURRENT_FH set
- * to either the parent directory, or to the file itself */
- switch (claim->claim) {
- @@ -524,6 +556,16 @@ int nfs41_open(
- putfh_args[0].in_recovery = 0;
- }
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (is_stream && (stream_comp.len > 0)) {
- + compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
- + lookup_args.name = &base_comp;
- + compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
- + /* If we are creating the stream, we should allow creating the attr dir */
- + openattr_args.createdir = (create == OPEN4_CREATE) ? TRUE : FALSE;
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- compound_add_op(&compound, OP_OPEN, &open_args, &open_res);
- open_args.seqid = 0;
- #ifdef DISABLE_FILE_DELEGATIONS
- @@ -549,6 +591,27 @@ int nfs41_open(
- parent->fh.superblock, &createattrs->attrmask);
- }
- open_args.claim = claim;
- +
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (is_stream &&
- + ((claim->claim == CLAIM_NULL) ||
- + (claim->claim == CLAIM_DELEGATE_CUR))) {
- + if (claim->claim == CLAIM_NULL) {
- + if (stream_comp.len > 0)
- + claim->u.null.filename = &stream_comp;
- + else
- + claim->u.null.filename = &base_comp;
- + }
- + else if (claim->claim == CLAIM_DELEGATE_CUR) {
- + claim->u.deleg_cur.name = &stream_comp;
- + if (stream_comp.len > 0)
- + claim->u.deleg_cur.name = &stream_comp;
- + else
- + claim->u.deleg_cur.name = &base_comp;
- + }
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- open_res.resok4.stateid = stateid;
- open_res.resok4.delegation = delegation;
- @@ -582,6 +645,7 @@ int nfs41_open(
- if (compound_error(status = compound.res.status))
- goto out;
- + /* This can happen if |nfs41_open()| is called by the EA code */
- if (dir_info.type == NF4ATTRDIR) {
- file->fh.superblock = parent->fh.superblock;
- goto out;
- @@ -614,8 +678,8 @@ int nfs41_create(
- {
- int status;
- nfs41_compound compound;
- - nfs_argop4 argops[8];
- - nfs_resop4 resops[8];
- + nfs_argop4 argops[12];
- + nfs_resop4 resops[12];
- nfs41_sequence_args sequence_args;
- nfs41_sequence_res sequence_res;
- nfs41_putfh_args putfh_args;
- @@ -629,6 +693,35 @@ int nfs41_create(
- nfs41_file_info dir_info;
- nfs41_savefh_res savefh_res;
- nfs41_restorefh_res restorefh_res;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + char base_buf[NFS41_MAX_PATH_LEN+1];
- + char stream_buf[NFS41_MAX_COMPONENT_LEN+1];
- + bool is_stream = false;
- + nfs41_component base_comp = {0};
- + nfs41_component stream_comp = {0};
- + nfs41_lookup_args lookup_args;
- + nfs41_lookup_res lookup_res;
- + nfs41_openattr_args openattr_args;
- + nfs41_openattr_res openattr_res;
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (file->name.name) {
- + if (is_stream_path_fh(file)) {
- + status = parse_win32stream_name(file->name.name,
- + &is_stream, base_buf, stream_buf);
- + if (status)
- + goto out;
- +
- + if (is_stream) {
- + base_comp.name = base_buf;
- + base_comp.len = (unsigned short)strlen(base_buf);
- + stream_comp.name = stream_buf;
- + stream_comp.len = (unsigned short)strlen(stream_buf);
- + }
- + }
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- nfs41_superblock_getattr_mask(parent->fh.superblock, &attr_request);
- @@ -644,13 +737,34 @@ int nfs41_create(
- compound_add_op(&compound, OP_SAVEFH, NULL, &savefh_res);
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (is_stream && (stream_comp.len > 0)) {
- + compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
- + lookup_args.name = &base_comp;
- + compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
- + openattr_args.createdir = TRUE;
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- compound_add_op(&compound, OP_CREATE, &create_args, &create_res);
- create_args.objtype.type = type;
- if (type == NF4LNK) {
- create_args.objtype.u.lnk.linkdata = symlink;
- create_args.objtype.u.lnk.linkdata_len = (uint32_t)strlen(symlink);
- }
- - create_args.name = &file->name;
- +
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (is_stream) {
- + if (stream_comp.len > 0)
- + create_args.name = &stream_comp;
- + else
- + create_args.name = &base_comp;
- + }
- + else
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- + {
- + create_args.name = &file->name;
- + }
- create_args.createattrs = createattrs;
- nfs41_superblock_supported_attrs(
- parent->fh.superblock, &createattrs->attrmask);
- @@ -1283,8 +1397,8 @@ int nfs41_remove(
- {
- int status;
- nfs41_compound compound;
- - nfs_argop4 argops[4];
- - nfs_resop4 resops[4];
- + nfs_argop4 argops[8];
- + nfs_resop4 resops[8];
- nfs41_sequence_args sequence_args;
- nfs41_sequence_res sequence_res;
- nfs41_putfh_args putfh_args;
- @@ -1295,6 +1409,33 @@ int nfs41_remove(
- nfs41_getattr_res getattr_res NDSH(= { 0 });
- bitmap4 attr_request;
- nfs41_file_info info;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + char base_buf[NFS41_MAX_PATH_LEN+1];
- + char stream_buf[NFS41_MAX_COMPONENT_LEN+1];
- + bool is_stream = false;
- + nfs41_component base_comp = {0};
- + nfs41_component stream_comp = {0};
- + nfs41_lookup_args lookup_args;
- + nfs41_lookup_res lookup_res;
- + nfs41_openattr_args openattr_args;
- + nfs41_openattr_res openattr_res;
- +
- + if (target->name) {
- + if (is_stream_component(target)) {
- + status = parse_win32stream_name(target->name,
- + &is_stream, base_buf, stream_buf);
- + if (status)
- + goto out;
- +
- + if (is_stream) {
- + base_comp.name = base_buf;
- + base_comp.len = (unsigned short)strlen(base_buf);
- + stream_comp.name = stream_buf;
- + stream_comp.len = (unsigned short)strlen(stream_buf);
- + }
- + }
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- nfs41_superblock_getattr_mask(parent->fh.superblock, &attr_request);
- @@ -1308,8 +1449,28 @@ int nfs41_remove(
- putfh_args.file = parent;
- putfh_args.in_recovery = 0;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (is_stream && (stream_comp.len > 0)) {
- + compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
- + lookup_args.name = &base_comp;
- + compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
- + openattr_args.createdir = FALSE;
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- compound_add_op(&compound, OP_REMOVE, &remove_args, &remove_res);
- - remove_args.target = target;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (is_stream) {
- + if (stream_comp.len > 0)
- + remove_args.target = &stream_comp;
- + else
- + remove_args.target = &base_comp;
- + }
- + else
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- + {
- + remove_args.target = target;
- + }
- compound_add_op(&compound, OP_GETATTR, &getattr_args, &getattr_res);
- getattr_args.attr_request = &attr_request;
- @@ -1369,6 +1530,17 @@ int nfs41_rename(
- bitmap4 attr_request;
- nfs41_restorefh_res restorefh_res;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (src_name->name) {
- + EASSERT_MSG(is_stream_component(src_name) == false,
- + ("nfs41_rename: Streams not implemented yet"));
- + }
- + if (dst_name->name) {
- + EASSERT_MSG(is_stream_component(dst_name) == false,
- + ("nfs41_rename: Streams not implemented yet"));
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- nfs41_superblock_getattr_mask(src_dir->fh.superblock, &attr_request);
- compound_init(&compound, session->client->root->nfsminorvers,
- @@ -1555,6 +1727,21 @@ int nfs41_link(
- nfs41_file_info info = { 0 };
- nfs41_path_fh file;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (src->name.name) {
- + EASSERT_MSG(is_stream_path_fh(src) == false,
- + ("nfs41_link: Streams not implemented yet"));
- + }
- + if (dst_dir->name.name) {
- + EASSERT_MSG(is_stream_path_fh(dst_dir) == false,
- + ("nfs41_link: Streams not implemented yet"));
- + }
- + if (target->name) {
- + EASSERT_MSG(is_stream_component(target) == false,
- + ("nfs41_link: Streams not implemented yet"));
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- #ifdef NFS41_DRIVER_STABILITY_HACKS
- /* gisburn: fixme, see comment about |NDSH| above */
- (void)memset(getattr_res, 0, sizeof(getattr_res));
- @@ -1655,6 +1842,13 @@ int nfs41_readlink(
- nfs41_putfh_res putfh_res;
- nfs41_readlink_res readlink_res;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (file->name.name) {
- + EASSERT_MSG(is_stream_path_fh(file) == false,
- + ("nfs41_readlink: Streams not implemented yet"));
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- compound_init(&compound, session->client->root->nfsminorvers,
- argops, resops, "readlink");
- @@ -1700,6 +1894,13 @@ int nfs41_access(
- nfs41_access_args access_args;
- nfs41_access_res access_res;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (file->name.name) {
- + EASSERT_MSG(is_stream_path_fh(file) == false,
- + ("nfs41_access: Streams not implemented yet"));
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- compound_init(&compound, session->client->root->nfsminorvers,
- argops, resops, "access");
- @@ -1937,6 +2138,13 @@ int nfs41_secinfo(
- nfs41_secinfo_args secinfo_args;
- nfs41_secinfo_no_name_res secinfo_res;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (name->name) {
- + EASSERT_MSG(is_stream_component(name) == false,
- + ("nfs41_secinfo: Streams not implemented yet"));
- + }
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- compound_init(&compound, session->client->root->nfsminorvers,
- argops, resops, "secinfo");
- diff --git a/daemon/nfs41_superblock.c b/daemon/nfs41_superblock.c
- index 5ffb171..1a767d3 100644
- --- a/daemon/nfs41_superblock.c
- +++ b/daemon/nfs41_superblock.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -303,8 +303,12 @@ void nfs41_superblock_fs_attributes(
- FsAttrs->FileSystemAttributes |= FILE_SUPPORTS_HARD_LINKS;
- if (superblock->symlink_support)
- FsAttrs->FileSystemAttributes |= FILE_SUPPORTS_REPARSE_POINTS;
- - if (superblock->ea_support)
- + if (superblock->ea_support) {
- FsAttrs->FileSystemAttributes |= FILE_SUPPORTS_EXTENDED_ATTRIBUTES;
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + FsAttrs->FileSystemAttributes |= FILE_NAMED_STREAMS;
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- + }
- if (superblock->case_preserving)
- FsAttrs->FileSystemAttributes |= FILE_CASE_PRESERVED_NAMES;
- if (!superblock->case_insensitive)
- diff --git a/daemon/open.c b/daemon/open.c
- index b5e72c7..d9163bc 100644
- --- a/daemon/open.c
- +++ b/daemon/open.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -37,6 +37,7 @@
- #include "fileinfoutil.h"
- #include "util.h"
- #include "idmap.h"
- +#include "winstreams.h"
- static int create_open_state(
- IN const char *path,
- @@ -880,8 +881,12 @@ static int handle_open(void *daemon_context, nfs41_upcall *upcall)
- // first check if windows told us it's a directory
- if (args->create_opts & FILE_DIRECTORY_FILE)
- state->type = NF4DIR;
- - else
- - state->type = NF4REG;
- + else {
- + if (strstr(args->path, ":"))
- + state->type = NF4NAMEDATTR;
- + else
- + state->type = NF4REG;
- + }
- #ifdef NFS41_DRIVER_ALLOW_CREATEFILE_ACLS
- if (args->sec_desc_len) {
- @@ -1007,20 +1012,26 @@ static int handle_open(void *daemon_context, nfs41_upcall *upcall)
- goto out_free_state;
- }
- }
- - } else if (info.type == NF4REG) {
- + } else if ((info.type == NF4REG) || (info.type == NF4NAMEDATTR)) {
- DPRINTF(2, ("handle nfs41_open: FILE\n"));
- if (args->create_opts & FILE_DIRECTORY_FILE) {
- - DPRINTF(1, ("trying to open file '%s' as a directory\n",
- - state->path.path));
- + DPRINTF(1,
- + ("trying to open file state->path.path='%s',"
- + "info.type=%d as a directory\n",
- + state->path.path, (int)info.type));
- /*
- * Notes:
- * - SMB returns |STATUS_OBJECT_TYPE_MISMATCH|
- * while NTFS returns |STATUS_NOT_A_DIRECTORY|
- * - See |map_open_errors()| for the mapping to
- * |STATUS_*|
- + * - We do not return |ERROR_DIRECTORY| when opening a Win32
- + * stream
- */
- - status = ERROR_DIRECTORY;
- - goto out_free_state;
- + if (is_stream_path(&state->path) == false) {
- + status = ERROR_DIRECTORY;
- + goto out_free_state;
- + }
- }
- } else if (info.type == NF4LNK) {
- DPRINTF(2, ("handle nfs41_open: SYMLINK\n"));
- @@ -1324,7 +1335,7 @@ supersede_retry:
- #ifdef DEBUG_OPEN_SPARSE_FILES
- if ((status == 0) &&
- - (info.type == NF4REG) &&
- + (info.type == NF4REG | info.type == NF4NAMEDATTR) &&
- (state->session->client->root->supports_nfs42_seek)) {
- //debug_list_sparsefile_holes(state);
- debug_list_sparsefile_datasections(state);
- @@ -1524,7 +1535,7 @@ static int handle_close(void *deamon_context, nfs41_upcall *upcall)
- nfs41_open_state *state = upcall->state_ref;
- /* return associated file layouts if necessary */
- - if (state->type == NF4REG)
- + if (state->type == NF4REG || state->type == NF4NAMEDATTR)
- pnfs_layout_state_close(state->session, state, args->remove);
- if (state->srv_open == args->srv_open)
- diff --git a/daemon/upcall.h b/daemon/upcall.h
- index 1563c25..01a84ed 100644
- --- a/daemon/upcall.h
- +++ b/daemon/upcall.h
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -122,6 +122,10 @@ typedef struct __getattr_upcall_args {
- FILE_STAT_INFORMATION stat_info;
- FILE_STAT_LX_INFORMATION stat_lx_info;
- #endif /* NFS41_DRIVER_WSL_SUPPORT */
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + PFILE_STREAM_INFORMATION stream_info_list;
- + ULONG stream_info_list_size;
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- int query_class;
- int buf_len;
- int query_reply_len;
- diff --git a/daemon/winstreams.c b/daemon/winstreams.c
- new file mode 100644
- index 0000000..be0af14
- --- /dev/null
- +++ b/daemon/winstreams.c
- @@ -0,0 +1,455 @@
- +/* NFSv4.1 client for Windows
- + * Copyright (C) 2025-2026 Roland Mainz <roland.mainz@nrubsig.org>
- + *
- + * Roland Mainz <roland.mainz@nrubsig.org>
- + *
- + * This library is free software; you can redistribute it and/or modify it
- + * under the terms of the GNU Lesser General Public License as published by
- + * the Free Software Foundation; either version 2.1 of the License, or (at
- + * your option) any later version.
- + *
- + * This library is distributed in the hope that it will be useful, but
- + * without any warranty; without even the implied warranty of merchantability
- + * or fitness for a particular purpose. See the GNU Lesser General Public
- + * License for more details.
- + *
- + * You should have received a copy of the GNU Lesser General Public License
- + * along with this library; if not, write to the Free Software Foundation,
- + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA
- + */
- +
- +#include <stdlib.h>
- +#include <stdbool.h>
- +
- +#include "nfs41_build_features.h"
- +#include "winstreams.h"
- +#include "from_kernel.h"
- +#include "nfs41_ops.h"
- +#include "delegation.h"
- +#include "upcall.h"
- +#include "daemon_debug.h"
- +#include "util.h"
- +
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- +/*
- + * |WIN_NFS4_STREAMS_NAME_PREFIX| - Prefix for Windows streams in NFSv4
- + * XATTR (extended attributes) namespace
- + *
- + * We need such a prefix to avoid colliding with other users
- + * in the NFSv4 XATTR namespace - for example SUN Microsystrems
- + * (Solaris, Illumos, ...) uses "SUNWattr_" as prefix, and setting
- + * such attributes can cause data corruption (or in case of
- + * "SUNWattr_ro" will fail, because the attribute file is
- + * read-only).
- + */
- +#define WIN_NFS4_STREAMS_NAME_PREFIX "win32.streams."
- +#define WIN_NFS4_STREAMS_NAME_PREFIX_LEN (14)
- +
- +static
- +int parse_stream_filename_streamname_streamtype(
- + const char *restrict path,
- + char *restrict filename,
- + char *restrict streamname,
- + char *restrict streamtype)
- +{
- + const char *sep;
- + const char *base;
- + int colon_count;
- + const char *c1;
- + const char *c2;
- + const char *p;
- + size_t len_prefix;
- + size_t len_sn;
- +
- + filename[0] = '\0';
- + streamname[0] = '\0';
- + streamtype[0] = '\0';
- +
- + /* Take the last component after the last backslash */
- + sep = strrchr(path, '\\');
- + base = sep ? (sep + 1) : path;
- +
- + /* Count colons and error out if >= 3 in the final component */
- + colon_count = 0;
- + for (p = base ; *p ; p++) {
- + if (*p == ':') {
- + colon_count++;
- + if (colon_count >= 3)
- + return ERROR_INVALID_NAME;
- + }
- + }
- +
- + /* Find first colon (if any) */
- + c1 = strchr(base, ':');
- + if (c1 == NULL) {
- + /* No colon: filename is the entire path */
- + (void)strcpy(filename, path);
- + return NO_ERROR;
- + }
- +
- + /* First colon exists: filename before ':' must be non-empty */
- + if (c1 == base)
- + return ERROR_INVALID_NAME;
- +
- + /*
- + * One colon case
- + */
- + if (colon_count == 1) {
- + if (*(c1 + 1) == '\0')
- + return ERROR_INVALID_NAME; /* "filename:" => invalid */
- +
- + /* filename = full path up to first colon in base */
- + len_prefix = (size_t)(c1 - path);
- + (void)memcpy(filename, path, len_prefix);
- + filename[len_prefix] = '\0';
- +
- + /* streamname after first colon */
- + (void)strcpy(streamname, c1 + 1);
- + /* streamtype stays empty */
- + return NO_ERROR;
- + }
- +
- + /*
- + * Two colons case
- + */
- + c2 = strchr(c1 + 1, ':');
- + if (c2 == NULL) {
- + /* Should not happen when colon_count == 2 */
- + return ERROR_INVALID_NAME;
- + }
- +
- + if (*(c2 + 1) == '\0')
- + return ERROR_INVALID_NAME; /* type must be non-empty */
- +
- + /* filename = full path up to first colon */
- + len_prefix = (size_t)(c1 - path);
- + (void)memcpy(filename, path, len_prefix);
- + filename[len_prefix] = '\0';
- +
- + /* streamname = [c1+1, c2] (may be empty, e.g., filename::$DATA) */
- + len_sn = (size_t)(c2 - (c1 + 1));
- + (void)memcpy(streamname, c1 + 1, len_sn);
- + streamname[len_sn] = '\0';
- +
- + /* streamtype = (c2+1 .. end) */
- + (void)strcpy(streamtype, c2 + 1);
- +
- + return NO_ERROR;
- +}
- +
- +int parse_win32stream_name(
- + IN const char *restrict path,
- + OUT bool *restrict is_stream,
- + OUT char *restrict base_name,
- + OUT char *restrict stream_name)
- +{
- + int status;
- + char filenamebuff[NFS41_MAX_PATH_LEN+1];
- + char streamnamebuff[NFS41_MAX_COMPONENT_LEN+1];
- + /* |streamtypebuff| must include space for prefix+suffix */
- + char streamtypebuff[NFS41_MAX_COMPONENT_LEN+1+128];
- + char *p;
- +
- + status = parse_stream_filename_streamname_streamtype(path,
- + filenamebuff, streamnamebuff, streamtypebuff);
- + if (status) {
- + eprintf("parse_win32stream_name: "
- + "parsing for path='%s' failed, status=%d\n",
- + path, status);
- + return status;
- + }
- +
- + DPRINTF(2,
- + ("parse_win32stream_name: "
- + "parse_stream_filename_streamname_streamtype(path='%s') returned "
- + "filenamebuff='%s', streamnamebuff='%s', streamtypebuff='%s'\n",
- + path, filenamebuff, streamnamebuff, streamtypebuff));
- +
- + if ((streamnamebuff[0] == '\0') && (streamtypebuff[0] == '\0')) {
- + return ERROR_INVALID_NAME;
- + }
- +
- + /* We do not support any stream types except "$DATA" (yet) */
- + if ((streamtypebuff[0] != '\0') &&
- + ((_stricmp(streamtypebuff, "$DATA") != 0))) {
- + eprintf("parse_win32stream_name: "
- + "Unsupported stream type, path='%s', "
- + "stream='%s', streamtype='%s'\n",
- + path,
- + streamnamebuff,
- + streamtypebuff);
- + return ERROR_INVALID_NAME;
- + }
- +
- + /* "foo::$DATA" refers to "foo" */
- + if (streamnamebuff[0] == '\0') {
- + *is_stream = true;
- + (void)strcpy(base_name, filenamebuff);
- + stream_name[0] = '\0';
- + return NO_ERROR;
- + }
- +
- + /*
- + * If we have a stream name, then add our NFS attr prefix for Windows
- + * streams, and ":$DATA" as suffix
- + */
- + *is_stream = true;
- + (void)strcpy(base_name, filenamebuff);
- + p = stpcpy(stream_name, WIN_NFS4_STREAMS_NAME_PREFIX);
- + p = stpcpy(p, streamnamebuff);
- + (void)stpcpy(p, ":$DATA");
- +
- + return NO_ERROR;
- +}
- +
- +#define READDIR_LEN_INITIAL 8192
- +#define READDIR_LEN_MIN 2048
- +
- +/* call readdir repeatedly to get a complete list of entries */
- +static
- +int read_entire_dir(
- + IN nfs41_session *session,
- + IN nfs41_path_fh *dir,
- + OUT unsigned char **restrict buffer_out,
- + OUT uint32_t *restrict length_out)
- +{
- + nfs41_readdir_cookie cookie = { 0 };
- + nfs41_readdir_entry *last_entry;
- + unsigned char *buffer;
- + uint32_t buffer_len, len, total_len;
- + bool_t eof;
- + int status = NO_ERROR;
- + /* Attributes for stream */
- + bitmap4 attr_request = {
- + .count = 2,
- + .arr = {
- + [0] = FATTR4_WORD0_TYPE | FATTR4_WORD0_CHANGE |
- + FATTR4_WORD0_SIZE | FATTR4_WORD0_FSID |
- + FATTR4_WORD0_FILEID,
- + [1] = FATTR4_WORD1_SPACE_USED,
- + [2] = 0
- + }
- + };
- +
- + /* allocate the buffer for readdir entries */
- + buffer_len = READDIR_LEN_INITIAL;
- + buffer = calloc(1, buffer_len);
- + if (buffer == NULL) {
- + status = GetLastError();
- + goto out;
- + }
- +
- + last_entry = NULL;
- + total_len = 0;
- + eof = FALSE;
- +
- + while (!eof) {
- + len = buffer_len - total_len;
- + if (len < READDIR_LEN_MIN) {
- + const ptrdiff_t diff = (unsigned char*)last_entry - buffer;
- + /* realloc the buffer to fit more entries */
- + unsigned char *tmp = realloc(buffer, (size_t)buffer_len * 2L);
- + if (tmp == NULL) {
- + status = GetLastError();
- + goto out_free;
- + }
- +
- + if (last_entry) /* fix last_entry pointer */
- + last_entry = (nfs41_readdir_entry*)(tmp + diff);
- + buffer = tmp;
- + buffer_len *= 2;
- + len = buffer_len - total_len;
- + }
- +
- + /* fetch the next group of entries */
- + status = nfs41_readdir(session, dir, &attr_request,
- + &cookie, buffer + total_len, &len, &eof);
- + if (status)
- + goto out_free;
- +
- + if (last_entry == NULL) {
- + /* initialize last_entry to the front of the list */
- + last_entry = (nfs41_readdir_entry *)(buffer + total_len);
- + } else if (len) {
- + /* link the previous list to the new one */
- + last_entry->next_entry_offset = (uint32_t)FIELD_OFFSET(
- + nfs41_readdir_entry, name) + last_entry->name_len;
- + }
- +
- + /* find the new last entry */
- + while (last_entry->next_entry_offset) {
- + last_entry = (nfs41_readdir_entry*)((char*)last_entry +
- + last_entry->next_entry_offset);
- + }
- +
- + cookie.cookie = last_entry->cookie;
- + total_len += len;
- + }
- +
- + *buffer_out = buffer;
- + *length_out = total_len;
- +out:
- + return status;
- +
- +out_free:
- + free(buffer);
- + goto out;
- +}
- +
- +#define ALIGNED_STREAMINFOSIZE(namebytelen) \
- + (align8(sizeof(FILE_STREAM_INFORMATION) + (namebytelen)))
- +
- +static
- +uint32_t calculate_stream_list_length(
- + IN nfs41_component *streamfilename,
- + IN const unsigned char *restrict position,
- + IN uint32_t remaining)
- +{
- + const nfs41_readdir_entry *entry;
- + uint32_t length = 0;
- +
- + /* FIXME: We always have to add a dummy ::$DATA entry for the default stream! */
- +
- + while (remaining) {
- + entry = (const nfs41_readdir_entry *)position;
- +
- + if ((entry->name_len > WIN_NFS4_STREAMS_NAME_PREFIX_LEN) &&
- + (memcmp(entry->name,
- + WIN_NFS4_STREAMS_NAME_PREFIX,
- + WIN_NFS4_STREAMS_NAME_PREFIX_LEN) == 0)) {
- + /* FIXME: We should return the correct length of the UTF-8 string in |WCHAR| bytes*/
- + length +=
- + ALIGNED_STREAMINFOSIZE((streamfilename->len + 1 + entry->name_len - WIN_NFS4_STREAMS_NAME_PREFIX_LEN)*sizeof(WCHAR));
- + }
- +
- + if (!entry->next_entry_offset)
- + break;
- +
- + position += entry->next_entry_offset;
- + remaining -= entry->next_entry_offset;
- + }
- + return length;
- +}
- +
- +static
- +void populate_stream_list(
- + IN nfs41_component *streamfilename,
- + IN const unsigned char *position,
- + OUT FILE_STREAM_INFORMATION *restrict stream_list)
- +{
- + const nfs41_readdir_entry *entry;
- + PFILE_STREAM_INFORMATION stream = stream_list;
- + PFILE_STREAM_INFORMATION last_win_stream = NULL;
- + bool is_win_stream;
- +
- + /* FIXME: We always have to add a dummy ::$DATA entry for the default stream! */
- +
- + for (;;) {
- + entry = (const nfs41_readdir_entry *)position;
- +
- + if ((entry->name_len > WIN_NFS4_STREAMS_NAME_PREFIX_LEN) &&
- + (memcmp(entry->name,
- + WIN_NFS4_STREAMS_NAME_PREFIX,
- + WIN_NFS4_STREAMS_NAME_PREFIX_LEN) == 0)) {
- + is_win_stream = true;
- + }
- + else {
- + is_win_stream = false;
- + }
- +
- + if (is_win_stream) {
- + size_t streamnamelen_chars =
- + (streamfilename->len + 1 + entry->name_len - WIN_NFS4_STREAMS_NAME_PREFIX_LEN);
- + stream->StreamNameLength = (ULONG)(streamnamelen_chars*sizeof(WCHAR));
- +
- + EASSERT(bitmap_isset(&entry->attr_info.attrmask, 0,
- + FATTR4_WORD0_SIZE));
- + EASSERT(bitmap_isset(&entry->attr_info.attrmask, 1,
- + FATTR4_WORD1_SPACE_USED));
- + stream->StreamSize.QuadPart = entry->attr_info.size;
- + stream->StreamAllocationSize.QuadPart = entry->attr_info.space_used;
- +
- + /* FIXME: Unicode!! */
- + ULONG i;
- + wchar_t *ssn = stream->StreamName;
- + for (i=0 ; i < streamfilename->len ; i++)
- + *ssn++ = streamfilename->name[i];
- + *ssn++ = L':';
- + for (i=0 ; i < streamnamelen_chars ; i++)
- + *ssn++ = entry->name[i+WIN_NFS4_STREAMS_NAME_PREFIX_LEN];
- +
- + DPRINTF(0,
- + ("populate_streams_list: adding stream "
- + "entry->(name='%.*s' name_len=%d) "
- + "stream->(StreamName='%.*ls', StreamNameLength=%d, "
- + "StreamSize=%lld, StreamAllocationSize=%lld)\n",
- + (int)entry->name_len,
- + entry->name,
- + (int)entry->name_len,
- + (int)stream->StreamNameLength/sizeof(WCHAR),
- + stream->StreamName,
- + (int)stream->StreamNameLength,
- + (long long)stream->StreamSize.QuadPart,
- + (long long)stream->StreamAllocationSize.QuadPart));
- + last_win_stream = stream;
- + }
- +
- + if (!entry->next_entry_offset) {
- + (last_win_stream?last_win_stream:stream)->NextEntryOffset = 0;
- + break;
- + }
- +
- +#define STREAMINFO_NEXT_ENTRY(str) ((PBYTE)(str) + (str)->NextEntryOffset)
- + if (is_win_stream) {
- + stream->NextEntryOffset =
- + ALIGNED_STREAMINFOSIZE(stream->StreamNameLength);
- + stream = (PFILE_STREAM_INFORMATION)STREAMINFO_NEXT_ENTRY(stream);
- + }
- +
- + position += entry->next_entry_offset;
- + }
- +}
- +
- +/*static*/
- +int get_stream_list(
- + IN OUT nfs41_open_state *state,
- + IN nfs41_path_fh *streamfile,
- + OUT PFILE_STREAM_INFORMATION *restrict streamlist_out,
- + OUT ULONG *streamlist_out_size)
- +{
- + unsigned char *entry_list;
- + PFILE_STREAM_INFORMATION stream_list;
- + uint32_t entry_len, stream_list_size;
- + int status = NO_ERROR;
- +
- + /* read the entire directory into a |nfs41_readdir_entry| buffer */
- + status = read_entire_dir(state->session, streamfile,
- + &entry_list, &entry_len);
- + if (status)
- + goto out;
- +
- + stream_list_size = calculate_stream_list_length(&streamfile->name, entry_list, entry_len);
- + if (stream_list_size == 0) {
- + *streamlist_out = NULL;
- + *streamlist_out_size = 0UL;
- + goto out_free;
- + }
- + stream_list = calloc(1, stream_list_size);
- + if (stream_list == NULL) {
- + status = GetLastError();
- + goto out_free;
- + }
- +
- + populate_stream_list(&streamfile->name, entry_list, stream_list);
- +
- + DPRINTF(0, ("get_stream_list: stream_list=%p, stream_list_size=%ld\n",
- + stream_list, (long)stream_list_size));
- + *streamlist_out = stream_list;
- + *streamlist_out_size = stream_list_size;
- +out_free:
- + free(entry_list); /* allocated by |read_entire_dir()| */
- +out:
- + return status;
- +}
- +
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- diff --git a/daemon/winstreams.h b/daemon/winstreams.h
- new file mode 100644
- index 0000000..f102afb
- --- /dev/null
- +++ b/daemon/winstreams.h
- @@ -0,0 +1,73 @@
- +/* NFSv4.1 client for Windows
- + * Copyright (C) 2025-2026 Roland Mainz <roland.mainz@nrubsig.org>
- + *
- + * Roland Mainz <roland.mainz@nrubsig.org>
- + *
- + * This library is free software; you can redistribute it and/or modify it
- + * under the terms of the GNU Lesser General Public License as published by
- + * the Free Software Foundation; either version 2.1 of the License, or (at
- + * your option) any later version.
- + *
- + * This library is distributed in the hope that it will be useful, but
- + * without any warranty; without even the implied warranty of merchantability
- + * or fitness for a particular purpose. See the GNU Lesser General Public
- + * License for more details.
- + *
- + * You should have received a copy of the GNU Lesser General Public License
- + * along with this library; if not, write to the Free Software Foundation,
- + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA
- + */
- +
- +#ifndef __NFS41_DAEMON_WINSTREAMS_H__
- +#define __NFS41_DAEMON_WINSTREAMS_H__ 1
- +
- +#include <stdlib.h>
- +#include <stdbool.h>
- +
- +#include "nfs41_build_features.h"
- +#include "nfs41_types.h"
- +#include "from_kernel.h"
- +
- +
- +static __inline
- +bool is_stream_path(const nfs41_abs_path *restrict path)
- +{
- + if (memchr(path->path, ':', path->len) != NULL)
- + return true;
- + return false;
- +}
- +
- +static __inline
- +bool is_stream_path_fh(const nfs41_path_fh *restrict path)
- +{
- + if (memchr(path->name.name, ':', path->name.len) != NULL)
- + return true;
- + return false;
- +}
- +
- +static __inline
- +bool is_stream_component(const nfs41_component *restrict comp)
- +{
- + if (memchr(comp->name, ':', comp->len) != NULL)
- + return true;
- + return false;
- +}
- +
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- +
- +int parse_win32stream_name(
- + IN const char *restrict path,
- + OUT bool *restrict is_stream,
- + OUT char *restrict base_name,
- + OUT char *restrict stream_name);
- +
- +typedef struct __nfs41_open_state nfs41_open_state;
- +
- +int get_stream_list(
- + IN OUT nfs41_open_state *state,
- + IN nfs41_path_fh *streamdir,
- + OUT PFILE_STREAM_INFORMATION *restrict streamlist_out,
- + OUT ULONG *streamlist_out_size);
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- +
- +#endif /* !__NFS41_DAEMON_WINSTREAMS_H__ */
- diff --git a/include/from_kernel.h b/include/from_kernel.h
- index b9640a1..c0a24b3 100644
- --- a/include/from_kernel.h
- +++ b/include/from_kernel.h
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -191,6 +191,14 @@ typedef struct _FILE_RENAME_INFORMATION {
- WCHAR FileName[1];
- } FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION;
- +typedef struct _FILE_STREAM_INFORMATION {
- + ULONG NextEntryOffset;
- + ULONG StreamNameLength;
- + LARGE_INTEGER StreamSize;
- + LARGE_INTEGER StreamAllocationSize;
- + WCHAR StreamName[1];
- +} FILE_STREAM_INFORMATION, *PFILE_STREAM_INFORMATION;
- +
- /* FileAttributeTagInformation==35 */
- typedef struct _FILE_ATTRIBUTE_TAG_INFORMATION {
- ULONG FileAttributes;
- diff --git a/nfs41_build_features.h b/nfs41_build_features.h
- index fb744f5..227008f 100644
- --- a/nfs41_build_features.h
- +++ b/nfs41_build_features.h
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -273,4 +273,10 @@
- */
- #define NFS41_DRIVER_ALLOW_CREATEFILE_ACLS 1
- +/*
- + * |NFS41_WINSTREAMS_SUPPORT| - Enable Win32 named streams support using
- + * NFSv4.1 named attributes
- + */
- +#define NFS41_WINSTREAMS_SUPPORT 1
- +
- #endif /* !_NFS41_DRIVER_BUILDFEATURES_ */
- diff --git a/sys/nfs41sys_fileinfo.c b/sys/nfs41sys_fileinfo.c
- index f871915..f12b4e8 100644
- --- a/sys/nfs41sys_fileinfo.c
- +++ b/sys/nfs41sys_fileinfo.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -171,8 +171,19 @@ void unmarshal_nfs41_getattr(
- nfs41_updowncall_entry *cur,
- const unsigned char *restrict *restrict buf)
- {
- - unmarshal_nfs41_attrget(cur,
- - cur->u.QueryFile.buf, &cur->u.QueryFile.buf_len, buf, FALSE);
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if (cur->u.QueryFile.InfoClass == FileStreamInformation) {
- + /* FIXME: If we do a partial read, what happens to ChangeTime below ? */
- + unmarshal_nfs41_attrget(cur,
- + cur->u.QueryFile.buf, &cur->u.QueryFile.buf_len, buf, TRUE);
- + }
- + else
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- + {
- + unmarshal_nfs41_attrget(cur,
- + cur->u.QueryFile.buf, &cur->u.QueryFile.buf_len, buf, FALSE);
- + }
- +
- RtlCopyMemory(&cur->ChangeTime, *buf, sizeof(cur->ChangeTime));
- *buf += sizeof(cur->ChangeTime);
- #ifdef DEBUG_MARSHAL_DETAIL
- @@ -398,6 +409,9 @@ NTSTATUS nfs41_QueryFileInformation(
- case FileStatInformation:
- case FileStatLxInformation:
- #endif /* NFS41_DRIVER_WSL_SUPPORT */
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + case FileStreamInformation:
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- break;
- default:
- print_error("nfs41_QueryFileInformation: "
- @@ -432,7 +446,23 @@ NTSTATUS nfs41_QueryFileInformation(
- print_error("nfs41_QueryFileInformation: "
- "entry->status == STATUS_BUFFER_TOO_SMALL\n");
- status = STATUS_BUFFER_TOO_SMALL;
- - } else if (entry->status == STATUS_SUCCESS) {
- + }
- + else
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + if ((InfoClass == FileStreamInformation) &&
- + (entry->status == STATUS_BUFFER_OVERFLOW)) {
- + /*
- + * |FileStreamInformation| must return |STATUS_BUFFER_OVERFLOW| if
- + * the buffer is too small to store all data
- + */
- + RxContext->InformationToReturn = entry->u.QueryFile.buf_len;
- + print_error("nfs41_QueryFileInformation: "
- + "FileStreamInformation: "
- + "entry->status == STATUS_BUFFER_OVERFLOW\n");
- + status = STATUS_BUFFER_OVERFLOW;
- + } else
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- + if (entry->status == STATUS_SUCCESS) {
- #ifdef DEBUG_FILE_QUERY
- print_error("nfs41_QueryFileInformation: entry->status == STATUS_SUCCESS\n");
- #endif
- @@ -500,6 +530,9 @@ NTSTATUS nfs41_QueryFileInformation(
- case FileStatInformation:
- case FileStatLxInformation:
- #endif /* NFS41_DRIVER_WSL_SUPPORT */
- +#ifdef NFS41_WINSTREAMS_SUPPORT
- + case FileStreamInformation:
- +#endif /* NFS41_WINSTREAMS_SUPPORT */
- break;
- default:
- print_error("nfs41_QueryFileInformation: "
- diff --git a/sys/nfs41sys_openclose.c b/sys/nfs41sys_openclose.c
- index 93c95b1..8c24ad1 100644
- --- a/sys/nfs41sys_openclose.c
- +++ b/sys/nfs41sys_openclose.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -574,11 +574,13 @@ NTSTATUS check_nfs41_create_args(
- goto out;
- }
- +#ifndef NFS41_WINSTREAMS_SUPPORT
- if (isStream(SrvOpen->pAlreadyPrefixedName)) {
- DbgP("nfs41_Create: Streams not supported (yet)\n");
- status = STATUS_NOT_SUPPORTED;
- goto out;
- }
- +#endif /* !NFS41_WINSTREAMS_SUPPORT */
- if (pVNetRootContext->read_only &&
- (params->DesiredAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA))) {
- diff --git a/sys/nfs41sys_updowncall.c b/sys/nfs41sys_updowncall.c
- index bc9093e..755798b 100644
- --- a/sys/nfs41sys_updowncall.c
- +++ b/sys/nfs41sys_updowncall.c
- @@ -1,6 +1,6 @@
- /* NFSv4.1 client for Windows
- * Copyright (C) 2012 The Regents of the University of Michigan
- - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Olga Kornievskaia <aglo@umich.edu>
- * Casey Bodley <cbodley@umich.edu>
- @@ -807,14 +807,18 @@ NTSTATUS nfs41_downcall(
- if ((header_tmp->opcode != NFS41_SYSOP_VOLUME_QUERY) &&
- (bytesread_from_inbuf != inbuf_len)) {
- print_error("nfs41_downcall: ASSERT: '%s' (xid=%lld): "
- - "(inbuf(=0x%p)-inbuf_orig(=0x%p))(=%ld) != inbuf_len(=%ld)\n",
- + "(inbuf(=0x%p)-inbuf_orig(=0x%p))(=%ld) != inbuf_len(=%ld), "
- + "cur->status=%d\n",
- opcode2string(header_tmp->opcode),
- cur->xid,
- inbuf,
- inbuf_orig,
- (long)bytesread_from_inbuf,
- - (long)inbuf_len);
- - status = STATUS_BUFFER_OVERFLOW;
- + (long)inbuf_len,
- + cur->status);
- + if (cur->status == STATUS_SUCCESS) {
- + status = STATUS_BUFFER_OVERFLOW;
- + }
- }
- }
- ExReleaseFastMutexUnsafe(&cur->lock);
- diff --git a/tests/manual_testing.txt b/tests/manual_testing.txt
- index a520bfb..386ba0f 100644
- --- a/tests/manual_testing.txt
- +++ b/tests/manual_testing.txt
- @@ -1,5 +1,5 @@
- #
- -# ms-nfs41-client manual testing sequence, 2025-12-12
- +# ms-nfs41-client manual testing sequence, 2026-01-02
- #
- # Draft version, needs to be turned into automated tests
- # if possible
- @@ -218,6 +218,21 @@ $ rm -f d1 d2 d1.diff ; printf '1\n2\n' >d1 ; cp d1 d2 ; printf '3\n' >> d2 ; di
- The test should print "# test OK"
- +#
- +# Tests for Win32 streams
- +#
- +
- +# this test should print $'dir_line1\ndir_line2\n'
- +rm -Rf dir1 && mkdir dir1 && cmd /c 'echo dir_line1 >>dir1:dirstr1 & echo dir_line2 >>dir1:dirstr1 & cat <dir1:dirstr1'
- +# these two tests should print $'file1data\nfile1_line1\nfile1_line2\n'
- +rm -f file1 && echo "file1data" >file1 && cmd /c 'echo file1_line1 >>file1:filestr1 & echo file1_line2 >>file1:filestr1:$DATA & C:\cygwin64\bin\cat.exe <file1 & C:\cygwin64\bin\cat.exe <file1:filestr1'
- +rm -f file1 && echo "file1data" >file1 && cmd /c 'echo file1_line1 >>file1:filestr1 & echo file1_line2 >>file1:filestr1:$DATA && type file1' && powershell -Command 'Get-Content -Path .\file1 -Stream filestr1'# this should list stream "filestr1"
- +# list all streams for file "file1" (should be "filestr1")
- +powershell -Command 'Get-Item -LiteralPath file1 -Stream * | Select *'
- +
- +# FIXME: tests for stream removal, enumeration and sparse data
- +
- +
- #
- # Tests for Cycgwin/UWIN/SFU Nfs3Attr EA-based local uid/gid
- #
- diff --git a/tests/winfsinfo1/winfsinfo.c b/tests/winfsinfo1/winfsinfo.c
- index edc3255..74ac2b7 100644
- --- a/tests/winfsinfo1/winfsinfo.c
- +++ b/tests/winfsinfo1/winfsinfo.c
- @@ -1,7 +1,7 @@
- /*
- * MIT License
- *
- - * Copyright (c) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
- + * Copyright (c) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- @@ -1518,6 +1518,85 @@ done:
- return res;
- }
- +static
- +int get_filestreaminfo(const char *progname, const char *filename)
- +{
- + int res = EXIT_FAILURE;
- + NTSTATUS status;
- + IO_STATUS_BLOCK iostatus;
- + PFILE_STREAM_INFORMATION fsi = NULL;
- +
- + HANDLE fileHandle = CreateFileA(filename,
- + GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
- + FILE_FLAG_BACKUP_SEMANTICS, NULL);
- + if (fileHandle == INVALID_HANDLE_VALUE) {
- + (void)fprintf(stderr,
- + "%s: Error opening file '%s'. Last error was %d.\n",
- + progname,
- + filename,
- + (int)GetLastError());
- + return EXIT_FAILURE;
- + }
- +
- +#define MAX_STREAM_INFOS (16)
- +#define FSI_MAXCHARS (256)
- + fsi = calloc(1,
- + (sizeof(FILE_STREAM_INFORMATION)+sizeof(wchar_t)*FSI_MAXCHARS)*MAX_STREAM_INFOS);
- + if (fsi == NULL) {
- + (void)fprintf(stderr,
- + "%s: Out of memory.\n",
- + progname);
- + return EXIT_FAILURE;
- + }
- +
- + status = ZwQueryInformationFile(fileHandle,
- + &iostatus,
- + fsi,
- + ((sizeof(FILE_STREAM_INFORMATION)+sizeof(wchar_t)*FSI_MAXCHARS)*MAX_STREAM_INFOS),
- + FileStreamInformation);
- +
- + if (status != STATUS_SUCCESS) {
- + (void)fprintf(stderr, "%s: ZwQueryInformationFile() "
- + "error. status==0x%lx.\n",
- + progname,
- + (long)status);
- + res = EXIT_FAILURE;
- + goto done;
- + }
- +
- + int streamindex;
- + FILE_STREAM_INFORMATION *stream;
- +
- + (void)printf("(\n");
- + (void)printf("\tfilename='%s'\n", filename);
- + (void)printf("\ttypeset -a streams=(\n");
- + for (stream = fsi, streamindex = 0 ; ; streamindex++) {
- + (void)printf("\t\t[%d]=(\n", streamindex);
- + (void)printf("\t\t\tStreamName='%.*ls'\n",
- + (int)(stream->StreamNameLength/sizeof(WCHAR)),
- + stream->StreamName);
- + (void)printf("\t\t\tStreamSize=%lld\n",
- + (long long)stream->StreamSize.QuadPart);
- + (void)printf("\t\t\tStreamAllocationSize=%lld\n",
- + (long long)stream->StreamAllocationSize.QuadPart);
- + (void)printf("\t\t)\n");
- +
- + if (stream->NextEntryOffset == 0)
- + break;
- +
- + stream = (FILE_STREAM_INFORMATION *)((char *)stream + stream->NextEntryOffset);
- + }
- + (void)printf("\t)\n");
- +
- + (void)printf(")\n");
- + res = EXIT_SUCCESS;
- +
- +done:
- + free(fsi);
- + (void)CloseHandle(fileHandle);
- + return res;
- +}
- +
- #define BUFFER_SIZE 2048
- static
- @@ -1769,6 +1848,7 @@ void usage(void)
- "fileremoteprotocolinfo|"
- "fileidinfo|"
- "filenetworkphysicalnameinfo|"
- + "filestreaminfo|"
- "fsctlqueryallocatedranges|"
- "get_wnetgetresourceinformation|"
- "get_wnetgetresourceparent"
- @@ -1851,6 +1931,9 @@ int main(int ac, char *av[])
- else if (!strcmp(subcmd, "filenetworkphysicalnameinfo")) {
- return get_filenetworkphysicalnameinfo(av[0], av[2]);
- }
- + else if (!strcmp(subcmd, "filestreaminfo")) {
- + return get_filestreaminfo(av[0], av[2]);
- + }
- else if (!strcmp(subcmd, "fsctlqueryallocatedranges")) {
- return fsctlqueryallocatedranges(av[0], av[2]);
- }
Win32 streams support
Posted by Anonymous on Fri 2nd Jan 2026 18:22
raw | new post
modification of post by Anonymous (view diff)
Submit a correction or amendment below (click here to make a fresh posting)
After submitting an amendment, you'll be able to view the differences between the old and new posts easily.
nrubsig.kpaste.net RSS