pastebin - collaborative debugging tool
rovema.kpaste.net RSS


msnfs41client: Patch to implement Win32 streams support via NFSv4.1 named attributes, 2026-01-05
Posted by Anonymous on Mon 5th Jan 2026 20:21
raw | new post

  1. From 0af9cbcf76548180278cf146fd4fb3d1c284d311 Mon Sep 17 00:00:00 2001
  2. From: Roland Mainz <roland.mainz@nrubsig.org>
  3. Date: Mon, 5 Jan 2026 21:09:28 +0100
  4. Subject: [PATCH] build.vc19,daemon,include,nfs41_build_features.h,sys,tests:
  5.  Implement Win32 streams support
  6.  
  7. Implement Win32 streams support via NFSv4.1 named attributes.
  8.  
  9. Reported-by: Lionel Cons <lionelcons1972@gmail.com>
  10. Reported-by: Cedric Blancher <cedric.blancher@gmail.com>
  11. Signed-off-by: Cedric Blancher <cedric.blancher@gmail.com>
  12. ---
  13. build.vc19/nfsd/nfsd.vcxproj         |   2 +
  14.  build.vc19/nfsd/nfsd.vcxproj.filters |   6 +
  15.  daemon/fileinfoutil.c                |   5 +-
  16.  daemon/getattr.c                     |  32 +-
  17.  daemon/lookup.c                      | 156 ++++++-
  18.  daemon/nfs41_ops.c                   | 222 +++++++++-
  19.  daemon/nfs41_superblock.c            |   8 +-
  20.  daemon/open.c                        |  47 ++-
  21.  daemon/upcall.h                      |   6 +-
  22.  daemon/winstreams.c                  | 595 +++++++++++++++++++++++++++
  23.  daemon/winstreams.h                  |  76 ++++
  24.  include/from_kernel.h                |  10 +-
  25.  nfs41_build_features.h               |   8 +-
  26.  sys/nfs41sys_fileinfo.c              |  42 +-
  27.  sys/nfs41sys_openclose.c             |   8 +-
  28.  sys/nfs41sys_updowncall.c            |  18 +-
  29.  sys/nfs41sys_util.c                  |  15 +-
  30.  sys/nfs41sys_util.h                  |   4 +-
  31.  tests/manual_testing.txt             |  21 +-
  32.  tests/winfsinfo1/winfsinfo.c         |  85 +++-
  33.  20 files changed, 1301 insertions(+), 65 deletions(-)
  34.  create mode 100644 daemon/winstreams.c
  35.  create mode 100644 daemon/winstreams.h
  36.  
  37. diff --git a/build.vc19/nfsd/nfsd.vcxproj b/build.vc19/nfsd/nfsd.vcxproj
  38. index 8589a2a..face630 100644
  39. --- a/build.vc19/nfsd/nfsd.vcxproj
  40. +++ b/build.vc19/nfsd/nfsd.vcxproj
  41. @@ -331,6 +331,7 @@
  42.      <ClCompile Include="..\..\daemon\upcall.c" />
  43.      <ClCompile Include="..\..\daemon\util.c" />
  44.      <ClCompile Include="..\..\daemon\volume.c" />
  45. +    <ClCompile Include="..\..\daemon\winstreams.c" />
  46.    </ItemGroup>
  47.    <ItemGroup>
  48.      <ClInclude Include="..\..\daemon\accesstoken.h" />
  49. @@ -356,6 +357,7 @@
  50.      <ClInclude Include="..\..\daemon\tree.h" />
  51.      <ClInclude Include="..\..\daemon\upcall.h" />
  52.      <ClInclude Include="..\..\daemon\util.h" />
  53. +    <ClInclude Include="..\..\daemon\winstreams.h" />
  54.      <ClInclude Include="..\..\include\from_kernel.h" />
  55.      <ClInclude Include="..\..\include\nfs_ea.h" />
  56.    </ItemGroup>
  57. diff --git a/build.vc19/nfsd/nfsd.vcxproj.filters b/build.vc19/nfsd/nfsd.vcxproj.filters
  58. index 134404f..8e7c552 100644
  59. --- a/build.vc19/nfsd/nfsd.vcxproj.filters
  60. +++ b/build.vc19/nfsd/nfsd.vcxproj.filters
  61. @@ -147,6 +147,9 @@
  62.      <ClCompile Include="..\..\daemon\volume.c">
  63.        <Filter>Source Files</Filter>
  64.      </ClCompile>
  65. +    <ClCompile Include="..\..\daemon\winstreams.c">
  66. +      <Filter>Source Files</Filter>
  67. +    </ClCompile>
  68.      <ClCompile Include="..\..\daemon\accesstoken.c">
  69.        <Filter>Source Files</Filter>
  70.      </ClCompile>
  71. @@ -227,5 +230,8 @@
  72.      <ClInclude Include="..\..\daemon\accesstoken.h">
  73.        <Filter>Header Files</Filter>
  74.      </ClInclude>
  75. +    <ClInclude Include="..\..\daemon\winstreams.h">
  76. +      <Filter>Header Files</Filter>
  77. +    </ClInclude>
  78.    </ItemGroup>
  79.  </Project>
  80. \ No newline at end of file
  81. diff --git a/daemon/fileinfoutil.c b/daemon/fileinfoutil.c
  82. index 0ed40c8..d745fd2 100644
  83. --- a/daemon/fileinfoutil.c
  84. +++ b/daemon/fileinfoutil.c
  85. @@ -1,6 +1,6 @@
  86.  /* NFSv4.1 client for Windows
  87.   * Copyright (C) 2012 The Regents of the University of Michigan
  88. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  89. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  90.   *
  91.   * Olga Kornievskaia <aglo@umich.edu>
  92.   * Casey Bodley <cbodley@umich.edu>
  93. @@ -59,7 +59,7 @@ ULONG nfs_file_info_to_attributes(
  94.          if (info->symlink_dir)
  95.              attrs |= FILE_ATTRIBUTE_DIRECTORY;
  96.      }
  97. -    else if (info->type == NF4REG) {
  98. +    else if ((info->type == NF4REG) || (info->type == NF4NAMEDATTR)) {
  99.          if (superblock->sparse_file_support) {
  100.              /* FIXME: What about pNFS ? */
  101.              attrs |= FILE_ATTRIBUTE_SPARSE_FILE;
  102. @@ -383,6 +383,7 @@ void nfs_to_stat_lx_info(
  103.      stat_lx_out->LxMode = 0UL;
  104.      switch(info->type) {
  105.          case NF4REG:
  106. +        case NF4NAMEDATTR:
  107.              stat_lx_out->LxMode |= LX_MODE_S_IFREG;
  108.              break;
  109.          case NF4DIR:
  110. diff --git a/daemon/getattr.c b/daemon/getattr.c
  111. index 4744285..470dd3e 100644
  112. --- a/daemon/getattr.c
  113. +++ b/daemon/getattr.c
  114. @@ -1,6 +1,6 @@
  115.  /* NFSv4.1 client for Windows
  116.   * Copyright (C) 2012 The Regents of the University of Michigan
  117. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  118. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  119.   *
  120.   * Olga Kornievskaia <aglo@umich.edu>
  121.   * Casey Bodley <cbodley@umich.edu>
  122. @@ -32,6 +32,9 @@
  123.  #include "upcall.h"
  124.  #include "fileinfoutil.h"
  125.  #include "daemon_debug.h"
  126. +#ifdef NFS41_WINSTREAMS_SUPPORT
  127. +#include "winstreams.h"
  128. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  129.  
  130.  
  131.  int nfs41_cached_getattr(
  132. @@ -204,6 +207,15 @@ static int handle_getattr(void *daemon_context, nfs41_upcall *upcall)
  133.              &args->stat_lx_info);
  134.          break;
  135.  #endif /* NFS41_DRIVER_WSL_SUPPORT */
  136. +#ifdef NFS41_WINSTREAMS_SUPPORT
  137. +    case FileStreamInformation:
  138. +        args->stream_info_list = NULL;
  139. +        status = get_streaminformation(state,
  140. +            &info,
  141. +            &args->stream_info_list,
  142. +            &args->stream_info_list_size);
  143. +        break;
  144. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  145.      default:
  146.          eprintf("handle_getattr(state->path.path='%s'): "
  147.              "unhandled file query class %d\n",
  148. @@ -222,7 +234,7 @@ static int marshall_getattr(
  149.      nfs41_upcall *restrict upcall)
  150.  {
  151.      int status;
  152. -    const getattr_upcall_args *args = &upcall->args.getattr;
  153. +    getattr_upcall_args *args = &upcall->args.getattr;
  154.      uint32_t info_len;
  155.  
  156.      switch (args->query_class) {
  157. @@ -292,6 +304,15 @@ static int marshall_getattr(
  158.          if (status) goto out;
  159.          break;
  160.  #endif /* NFS41_DRIVER_WSL_SUPPORT */
  161. +#ifdef NFS41_WINSTREAMS_SUPPORT
  162. +    case FileStreamInformation:
  163. +        info_len = args->stream_info_list_size;
  164. +        status = safe_write(&buffer, length, &info_len, sizeof(info_len));
  165. +        if (status) goto out;
  166. +        status = safe_write(&buffer, length, args->stream_info_list, info_len);
  167. +        if (status) goto out;
  168. +        break;
  169. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  170.      default:
  171.          eprintf("marshall_getattr: unknown file query class %d\n",
  172.              args->query_class);
  173. @@ -302,6 +323,13 @@ static int marshall_getattr(
  174.      if (status) goto out;
  175.      DPRINTF(1, ("NFS41_SYSOP_FILE_QUERY: downcall changattr=%llu\n", args->ctime));
  176.  out:
  177. +#ifdef NFS41_WINSTREAMS_SUPPORT
  178. +    if (args->query_class == FileStreamInformation) {
  179. +        free_streaminformation(args->stream_info_list);
  180. +        args->stream_info_list = NULL;
  181. +    }
  182. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  183. +
  184.      return status;
  185.  }
  186.  
  187. diff --git a/daemon/lookup.c b/daemon/lookup.c
  188. index 3e9e591..39074e2 100644
  189. --- a/daemon/lookup.c
  190. +++ b/daemon/lookup.c
  191. @@ -1,6 +1,6 @@
  192.  /* NFSv4.1 client for Windows
  193.   * Copyright (C) 2012 The Regents of the University of Michigan
  194. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  195. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  196.   *
  197.   * Olga Kornievskaia <aglo@umich.edu>
  198.   * Casey Bodley <cbodley@umich.edu>
  199. @@ -25,12 +25,16 @@
  200.  #include <strsafe.h>
  201.  #include <time.h>
  202.  
  203. +#include "nfs41_build_features.h"
  204.  #include "nfs41_compound.h"
  205.  #include "nfs41_ops.h"
  206.  #include "name_cache.h"
  207.  #include "fileinfoutil.h"
  208.  #include "util.h"
  209.  #include "daemon_debug.h"
  210. +#ifdef NFS41_WINSTREAMS_SUPPORT
  211. +#include "winstreams.h"
  212. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  213.  
  214.  
  215.  #define LULVL 2 /* dprintf level for lookup logging */
  216. @@ -502,6 +506,156 @@ int nfs41_lookup(
  217.  
  218.      DPRINTF(LULVL, ("--> nfs41_lookup('%s')\n", path.path));
  219.  
  220. +#ifdef NFS41_WINSTREAMS_SUPPORT
  221. +    /* Fast check for potential stream: Look for a colon */
  222. +    if (is_stream_path(path_inout)) {
  223. +        char base_name[NFS41_MAX_PATH_LEN+1];
  224. +        char stream_name[NFS41_MAX_COMPONENT_LEN+1];
  225. +        bool is_stream = false;
  226. +
  227. +        /* Parse the stream syntax */
  228. +        status = parse_win32stream_name(path.path, &is_stream, base_name, stream_name);
  229. +        if (status)
  230. +            goto out;
  231. +
  232. +        if (is_stream) {
  233. +            if (stream_name[0] == '\0') {
  234. +                /* "foo::$DATA" case -> treat as "foo" */
  235. +                DPRINTF(2, ("nfs41_lookup: 'foo::$DATA' case -> treat as 'foo'\n"));
  236. +                size_t base_len = strlen(base_name);
  237. +
  238. +                (void)memcpy(path.path, base_name, base_len);
  239. +                path.path[base_len] = '\0';
  240. +                path.len = (unsigned short)base_len;
  241. +                path_pos = path.path;
  242. +                path_end = path.path + path.len;
  243. +
  244. +                /* Continue to normal lookup with stripped name */
  245. +            } else {
  246. +                /* "foo:bar" case */
  247. +                DPRINTF(2, ("nfs41_lookup: base_name='%s', stream_name='%s'\n",
  248. +                    base_name, stream_name));
  249. +
  250. +                /*
  251. +                 * Recursively lookup the base file ...
  252. +                 */
  253. +                nfs41_abs_path base_path = path;
  254. +                size_t base_len = strlen(base_name);
  255. +
  256. +                (void)memcpy(base_path.path, base_name, base_len);
  257. +                base_path.path[base_len] = '\0';
  258. +                base_path.len = (unsigned short)base_len;
  259. +
  260. +                nfs41_path_fh attr_target;
  261. +                if (target_out == NULL) {
  262. +                    target_out = &attr_target;
  263. +                }
  264. +
  265. +                status = nfs41_lookup(root, session, casesensitive, &base_path,
  266. +                    parent_out, target_out, info_out, session_out);
  267. +                if (status)
  268. +                    goto out;
  269. +
  270. +                /*
  271. +                 * ... and then lookup the NFSv4 named attribute
  272. +                 */
  273. +                nfs41_session *lookup_session;
  274. +                nfs41_compound compound;
  275. +                nfs_argop4 argops[8];
  276. +                nfs_resop4 resops[8];
  277. +                nfs41_sequence_args sequence_args;
  278. +                nfs41_sequence_res sequence_res;
  279. +                nfs41_openattr_args openattr_args;
  280. +                nfs41_openattr_res openattr_res;
  281. +                nfs41_putfh_args putfh_args = { 0 };
  282. +                nfs41_putfh_res putfh_res;
  283. +                nfs41_lookup_args lookup_args = { 0 };
  284. +                nfs41_lookup_res lookup_res;
  285. +                nfs41_getfh_res getfh_res;
  286. +                nfs41_getattr_args getattr_args = { 0 };
  287. +                nfs41_getattr_res getattr_res = { 0 };
  288. +                nfs41_component stream_comp = {
  289. +                    .name = stream_name,
  290. +                    .len = (unsigned short)strlen(stream_name)
  291. +                };
  292. +                /* Minimal attributes for stream */
  293. +                bitmap4 attr_request = {
  294. +                    .count = 2,
  295. +                    .arr = {
  296. +                        [0] = FATTR4_WORD0_TYPE | FATTR4_WORD0_CHANGE |
  297. +                            FATTR4_WORD0_SIZE | FATTR4_WORD0_FSID |
  298. +                            FATTR4_WORD0_FILEID,
  299. +                        [1] = FATTR4_WORD1_MODE | FATTR4_WORD1_SPACE_USED |
  300. +                            FATTR4_WORD1_TIME_ACCESS |
  301. +                            FATTR4_WORD1_TIME_CREATE |
  302. +                            FATTR4_WORD1_TIME_MODIFY |
  303. +                            FATTR4_WORD1_OWNER | FATTR4_WORD1_OWNER_GROUP,
  304. +                        [2] = 0
  305. +                    }
  306. +                };
  307. +
  308. +                DPRINTF(1,
  309. +                    ("nfs41_lookup: "
  310. +                    "Looking up base_name='%s'/stream_name='%s'\n",
  311. +                    base_name, stream_name));
  312. +
  313. +                /* Use the session returned by the base lookup if applicable */
  314. +                if ((session_out != NULL) && (*session_out != NULL))
  315. +                    lookup_session = *session_out;
  316. +                else
  317. +                    lookup_session = session;
  318. +
  319. +                compound_init(&compound,
  320. +                    lookup_session->client->root->nfsminorvers,
  321. +                    argops, resops, "lookup_win32stream");
  322. +
  323. +                compound_add_op(&compound, OP_SEQUENCE, &sequence_args, &sequence_res);
  324. +                nfs41_session_sequence(&sequence_args, lookup_session, 0);
  325. +
  326. +                compound_add_op(&compound, OP_PUTFH, &putfh_args, &putfh_res);
  327. +                putfh_args.file = target_out;
  328. +                putfh_args.in_recovery = FALSE;
  329. +
  330. +                compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
  331. +                openattr_args.createdir = FALSE;
  332. +
  333. +                compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
  334. +                lookup_args.name = &stream_comp;
  335. +
  336. +                compound_add_op(&compound, OP_GETFH, NULL, &getfh_res);
  337. +                getfh_res.fh = &target_out->fh;
  338. +
  339. +                compound_add_op(&compound, OP_GETATTR, &getattr_args, &getattr_res);
  340. +                getattr_args.attr_request = &attr_request;
  341. +                /* If caller wanted info, fill it; else dummy */
  342. +                nfs41_file_info dummy_info;
  343. +                if (info_out)
  344. +                    getattr_res.info = info_out;
  345. +                else
  346. +                    getattr_res.info = &dummy_info;
  347. +                getattr_res.obj_attributes.attr_vals_len = NFS4_OPAQUE_LIMIT_ATTR;
  348. +
  349. +                status = compound_encode_send_decode(lookup_session, &compound, TRUE);
  350. +
  351. +                if (status == 0)
  352. +                    status = compound.res.status;
  353. +
  354. +                if (status == 0) {
  355. +                    /* We MUST return a |NF4NAMEDATTR|, and never a |NF4ATTRDIR| */
  356. +                    EASSERT(getattr_res.info->type != NF4ATTRDIR);
  357. +                    EASSERT(getattr_res.info->type == NF4NAMEDATTR);
  358. +                }
  359. +                else {
  360. +                    DPRINTF(1, ("nfs41_lookup: failed for attr, status=%d\n",
  361. +                        (int)status));
  362. +                }
  363. +
  364. +                goto out;
  365. +            }
  366. +        }
  367. +    }
  368. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  369. +
  370.      if (parent_out == NULL) parent_out = &parent;
  371.      if (target_out == NULL) target_out = &target;
  372.      parent_out->fh.len = target_out->fh.len = 0;
  373. diff --git a/daemon/nfs41_ops.c b/daemon/nfs41_ops.c
  374. index 9012db4..cc650e9 100644
  375. --- a/daemon/nfs41_ops.c
  376. +++ b/daemon/nfs41_ops.c
  377. @@ -1,6 +1,6 @@
  378.  /* NFSv4.1 client for Windows
  379.   * Copyright (C) 2012 The Regents of the University of Michigan
  380. - * Copyright (C) 2024-2025 Roland Mainz <roland.mainz@nrubsig.org>
  381. + * Copyright (C) 2024-2026 Roland Mainz <roland.mainz@nrubsig.org>
  382.   *
  383.   * Olga Kornievskaia <aglo@umich.edu>
  384.   * Casey Bodley <cbodley@umich.edu>
  385. @@ -36,6 +36,9 @@
  386.  #include "delegation.h"
  387.  #include "daemon_debug.h"
  388.  #include "util.h"
  389. +#ifdef NFS41_WINSTREAMS_SUPPORT
  390. +#include "winstreams.h"
  391. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  392.  
  393.  #ifdef NFS41_DRIVER_STABILITY_HACKS
  394.  /*
  395. @@ -453,8 +456,8 @@ int nfs41_open(
  396.  {
  397.      int status;
  398.      nfs41_compound compound;
  399. -    nfs_argop4 argops[8];
  400. -    nfs_resop4 resops[8];
  401. +    nfs_argop4 argops[12];
  402. +    nfs_resop4 resops[12];
  403.      nfs41_sequence_args sequence_args;
  404.      nfs41_sequence_res sequence_res;
  405.      nfs41_putfh_args putfh_args[2];
  406. @@ -471,10 +474,39 @@ int nfs41_open(
  407.      bool_t current_fh_is_dir;
  408.      bool_t already_delegated = delegation->type == OPEN_DELEGATE_READ
  409.          || delegation->type == OPEN_DELEGATE_WRITE;
  410. +#ifdef NFS41_WINSTREAMS_SUPPORT
  411. +    char base_buf[NFS41_MAX_PATH_LEN+1];
  412. +    char stream_buf[NFS41_MAX_COMPONENT_LEN+1];
  413. +    bool is_stream = false;
  414. +    nfs41_component base_comp = {0};
  415. +    nfs41_component stream_comp = {0};
  416. +    nfs41_lookup_args lookup_args;
  417. +    nfs41_lookup_res lookup_res;
  418. +    nfs41_openattr_args openattr_args;
  419. +    nfs41_openattr_res openattr_res;
  420. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  421.  
  422.      EASSERT_IS_VALID_NON_NULL_PTR(parent);
  423.      EASSERT_IS_VALID_NON_NULL_PTR(parent->fh.superblock);
  424.  
  425. +#ifdef NFS41_WINSTREAMS_SUPPORT
  426. +    if (file->name.name) {
  427. +        if (is_stream_path_fh(file)) {
  428. +            status = parse_win32stream_name(file->name.name,
  429. +                &is_stream, base_buf, stream_buf);
  430. +            if (status)
  431. +                goto out;
  432. +
  433. +            if (is_stream) {
  434. +                base_comp.name = base_buf;
  435. +                base_comp.len = (unsigned short)strlen(base_buf);
  436. +                stream_comp.name = stream_buf;
  437. +                stream_comp.len = (unsigned short)strlen(stream_buf);
  438. +            }
  439. +        }
  440. +    }
  441. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  442. +
  443.      /* depending on the claim type, OPEN expects CURRENT_FH set
  444.       * to either the parent directory, or to the file itself */
  445.      switch (claim->claim) {
  446. @@ -524,6 +556,16 @@ int nfs41_open(
  447.          putfh_args[0].in_recovery = 0;
  448.      }
  449.  
  450. +#ifdef NFS41_WINSTREAMS_SUPPORT
  451. +    if (is_stream && (stream_comp.len > 0)) {
  452. +        compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
  453. +        lookup_args.name = &base_comp;
  454. +        compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
  455. +        /* If we are creating the stream, we should allow creating the attr dir */
  456. +        openattr_args.createdir = (create == OPEN4_CREATE) ? TRUE : FALSE;
  457. +    }
  458. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  459. +
  460.      compound_add_op(&compound, OP_OPEN, &open_args, &open_res);
  461.      open_args.seqid = 0;
  462.  #ifdef DISABLE_FILE_DELEGATIONS
  463. @@ -549,6 +591,27 @@ int nfs41_open(
  464.              parent->fh.superblock, &createattrs->attrmask);
  465.      }
  466.      open_args.claim = claim;
  467. +
  468. +#ifdef NFS41_WINSTREAMS_SUPPORT
  469. +    if (is_stream &&
  470. +        ((claim->claim == CLAIM_NULL) ||
  471. +            (claim->claim == CLAIM_DELEGATE_CUR))) {
  472. +        if (claim->claim == CLAIM_NULL) {
  473. +            if (stream_comp.len > 0)
  474. +                claim->u.null.filename = &stream_comp;
  475. +            else
  476. +                claim->u.null.filename = &base_comp;
  477. +        }
  478. +        else if (claim->claim == CLAIM_DELEGATE_CUR) {
  479. +            claim->u.deleg_cur.name = &stream_comp;
  480. +            if (stream_comp.len > 0)
  481. +                claim->u.deleg_cur.name = &stream_comp;
  482. +            else
  483. +                claim->u.deleg_cur.name = &base_comp;
  484. +        }
  485. +    }
  486. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  487. +
  488.      open_res.resok4.stateid = stateid;
  489.      open_res.resok4.delegation = delegation;
  490.  
  491. @@ -582,7 +645,13 @@ int nfs41_open(
  492.      if (compound_error(status = compound.res.status))
  493.          goto out;
  494.  
  495. +    /* This can happen if |nfs41_open()| is called by the EA code */
  496.      if (dir_info.type == NF4ATTRDIR) {
  497. +#ifdef NFS41_WINSTREAMS_SUPPORT
  498. +        /* This codepath should not be triggered by Win32 streams! */
  499. +        EASSERT(!(is_stream && (stream_comp.len > 0)));
  500. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  501. +
  502.          file->fh.superblock = parent->fh.superblock;
  503.          goto out;
  504.      }
  505. @@ -614,8 +683,8 @@ int nfs41_create(
  506.  {
  507.      int status;
  508.      nfs41_compound compound;
  509. -    nfs_argop4 argops[8];
  510. -    nfs_resop4 resops[8];
  511. +    nfs_argop4 argops[12];
  512. +    nfs_resop4 resops[12];
  513.      nfs41_sequence_args sequence_args;
  514.      nfs41_sequence_res sequence_res;
  515.      nfs41_putfh_args putfh_args;
  516. @@ -629,6 +698,33 @@ int nfs41_create(
  517.      nfs41_file_info dir_info;
  518.      nfs41_savefh_res savefh_res;
  519.      nfs41_restorefh_res restorefh_res;
  520. +#ifdef NFS41_WINSTREAMS_SUPPORT
  521. +    char base_buf[NFS41_MAX_PATH_LEN+1];
  522. +    char stream_buf[NFS41_MAX_COMPONENT_LEN+1];
  523. +    bool is_stream = false;
  524. +    nfs41_component base_comp = {0};
  525. +    nfs41_component stream_comp = {0};
  526. +    nfs41_lookup_args lookup_args;
  527. +    nfs41_lookup_res lookup_res;
  528. +    nfs41_openattr_args openattr_args;
  529. +    nfs41_openattr_res openattr_res;
  530. +
  531. +    if (file->name.name) {
  532. +        if (is_stream_path_fh(file)) {
  533. +            status = parse_win32stream_name(file->name.name,
  534. +                &is_stream, base_buf, stream_buf);
  535. +            if (status)
  536. +                goto out;
  537. +
  538. +            if (is_stream) {
  539. +                base_comp.name = base_buf;
  540. +                base_comp.len = (unsigned short)strlen(base_buf);
  541. +                stream_comp.name = stream_buf;
  542. +                stream_comp.len = (unsigned short)strlen(stream_buf);
  543. +            }
  544. +        }
  545. +    }
  546. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  547.  
  548.      nfs41_superblock_getattr_mask(parent->fh.superblock, &attr_request);
  549.  
  550. @@ -644,13 +740,34 @@ int nfs41_create(
  551.  
  552.      compound_add_op(&compound, OP_SAVEFH, NULL, &savefh_res);
  553.  
  554. +#ifdef NFS41_WINSTREAMS_SUPPORT
  555. +    if (is_stream && (stream_comp.len > 0)) {
  556. +        compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
  557. +        lookup_args.name = &base_comp;
  558. +        compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
  559. +        openattr_args.createdir = TRUE;
  560. +    }
  561. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  562. +
  563.      compound_add_op(&compound, OP_CREATE, &create_args, &create_res);
  564.      create_args.objtype.type = type;
  565.      if (type == NF4LNK) {
  566.          create_args.objtype.u.lnk.linkdata = symlink;
  567.          create_args.objtype.u.lnk.linkdata_len = (uint32_t)strlen(symlink);
  568.      }
  569. -    create_args.name = &file->name;
  570. +
  571. +#ifdef NFS41_WINSTREAMS_SUPPORT
  572. +    if (is_stream) {
  573. +        if (stream_comp.len > 0)
  574. +            create_args.name = &stream_comp;
  575. +        else
  576. +            create_args.name = &base_comp;
  577. +    }
  578. +    else
  579. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  580. +    {
  581. +        create_args.name = &file->name;
  582. +    }
  583.      create_args.createattrs = createattrs;
  584.      nfs41_superblock_supported_attrs(
  585.                  parent->fh.superblock, &createattrs->attrmask);
  586. @@ -1283,8 +1400,8 @@ int nfs41_remove(
  587.  {
  588.      int status;
  589.      nfs41_compound compound;
  590. -    nfs_argop4 argops[4];
  591. -    nfs_resop4 resops[4];
  592. +    nfs_argop4 argops[8];
  593. +    nfs_resop4 resops[8];
  594.      nfs41_sequence_args sequence_args;
  595.      nfs41_sequence_res sequence_res;
  596.      nfs41_putfh_args putfh_args;
  597. @@ -1295,6 +1412,33 @@ int nfs41_remove(
  598.      nfs41_getattr_res getattr_res NDSH(= { 0 });
  599.      bitmap4 attr_request;
  600.      nfs41_file_info info;
  601. +#ifdef NFS41_WINSTREAMS_SUPPORT
  602. +    char base_buf[NFS41_MAX_PATH_LEN+1];
  603. +    char stream_buf[NFS41_MAX_COMPONENT_LEN+1];
  604. +    bool is_stream = false;
  605. +    nfs41_component base_comp = {0};
  606. +    nfs41_component stream_comp = {0};
  607. +    nfs41_lookup_args lookup_args;
  608. +    nfs41_lookup_res lookup_res;
  609. +    nfs41_openattr_args openattr_args;
  610. +    nfs41_openattr_res openattr_res;
  611. +
  612. +    if (target->name) {
  613. +        if (is_stream_component(target)) {
  614. +            status = parse_win32stream_name(target->name,
  615. +                &is_stream, base_buf, stream_buf);
  616. +            if (status)
  617. +                goto out;
  618. +
  619. +            if (is_stream) {
  620. +                base_comp.name = base_buf;
  621. +                base_comp.len = (unsigned short)strlen(base_buf);
  622. +                stream_comp.name = stream_buf;
  623. +                stream_comp.len = (unsigned short)strlen(stream_buf);
  624. +            }
  625. +        }
  626. +    }
  627. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  628.  
  629.      nfs41_superblock_getattr_mask(parent->fh.superblock, &attr_request);
  630.  
  631. @@ -1308,8 +1452,28 @@ int nfs41_remove(
  632.      putfh_args.file = parent;
  633.      putfh_args.in_recovery = 0;
  634.  
  635. +#ifdef NFS41_WINSTREAMS_SUPPORT
  636. +    if (is_stream && (stream_comp.len > 0)) {
  637. +        compound_add_op(&compound, OP_LOOKUP, &lookup_args, &lookup_res);
  638. +        lookup_args.name = &base_comp;
  639. +        compound_add_op(&compound, OP_OPENATTR, &openattr_args, &openattr_res);
  640. +        openattr_args.createdir = FALSE;
  641. +    }
  642. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  643. +
  644.      compound_add_op(&compound, OP_REMOVE, &remove_args, &remove_res);
  645. -    remove_args.target = target;
  646. +#ifdef NFS41_WINSTREAMS_SUPPORT
  647. +    if (is_stream) {
  648. +        if (stream_comp.len > 0)
  649. +            remove_args.target = &stream_comp;
  650. +        else
  651. +            remove_args.target = &base_comp;
  652. +    }
  653. +    else
  654. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  655. +    {
  656. +        remove_args.target = target;
  657. +    }
  658.  
  659.      compound_add_op(&compound, OP_GETATTR, &getattr_args, &getattr_res);
  660.      getattr_args.attr_request = &attr_request;
  661. @@ -1369,6 +1533,17 @@ int nfs41_rename(
  662.      bitmap4 attr_request;
  663.      nfs41_restorefh_res restorefh_res;
  664.  
  665. +#ifdef NFS41_WINSTREAMS_SUPPORT
  666. +    if (src_name->name) {
  667. +        EASSERT_MSG(is_stream_component(src_name) == false,
  668. +            ("nfs41_rename: Streams not implemented yet\n"));
  669. +    }
  670. +    if (dst_name->name) {
  671. +        EASSERT_MSG(is_stream_component(dst_name) == false,
  672. +            ("nfs41_rename: Streams not implemented yet\n"));
  673. +    }
  674. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  675. +
  676.      nfs41_superblock_getattr_mask(src_dir->fh.superblock, &attr_request);
  677.  
  678.      compound_init(&compound, session->client->root->nfsminorvers,
  679. @@ -1555,6 +1730,21 @@ int nfs41_link(
  680.      nfs41_file_info info = { 0 };
  681.      nfs41_path_fh file;
  682.  
  683. +#ifdef NFS41_WINSTREAMS_SUPPORT
  684. +    if (src->name.name) {
  685. +        EASSERT_MSG(is_stream_path_fh(src) == false,
  686. +            ("nfs41_link: Streams not implemented yet\n"));
  687. +    }
  688. +    if (dst_dir->name.name) {
  689. +        EASSERT_MSG(is_stream_path_fh(dst_dir) == false,
  690. +            ("nfs41_link: Streams not implemented yet\n"));
  691. +    }
  692. +    if (target->name) {
  693. +        EASSERT_MSG(is_stream_component(target) == false,
  694. +            ("nfs41_link: Streams not implemented yet\n"));
  695. +    }
  696. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  697. +
  698.  #ifdef NFS41_DRIVER_STABILITY_HACKS
  699.      /* gisburn: fixme, see comment about |NDSH| above */
  700.      (void)memset(getattr_res, 0, sizeof(getattr_res));
  701. @@ -1655,6 +1845,13 @@ int nfs41_readlink(
  702.      nfs41_putfh_res putfh_res;
  703.      nfs41_readlink_res readlink_res;
  704.  
  705. +#ifdef NFS41_WINSTREAMS_SUPPORT
  706. +    if (file->name.name) {
  707. +        EASSERT_MSG(is_stream_path_fh(file) == false,
  708. +            ("nfs41_readlink: Streams not implemented yet\n"));
  709. +    }
  710. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  711. +
  712.      compound_init(&compound, session->client->root->nfsminorvers,
  713.          argops, resops, "readlink");
  714.  
  715. @@ -1937,6 +2134,13 @@ int nfs41_secinfo(
  716.      nfs41_secinfo_args secinfo_args;
  717.      nfs41_secinfo_no_name_res secinfo_res;
  718.  
  719. +#ifdef NFS41_WINSTREAMS_SUPPORT
  720. +    if (name->name) {
  721. +        EASSERT_MSG(is_stream_component(name) == false,
  722. +            ("nfs41_secinfo: Streams not implemented yet\n"));
  723. +    }
  724. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  725. +
  726.      compound_init(&compound, session->client->root->nfsminorvers,
  727.          argops, resops, "secinfo");
  728.  
  729. diff --git a/daemon/nfs41_superblock.c b/daemon/nfs41_superblock.c
  730. index 5ffb171..1a767d3 100644
  731. --- a/daemon/nfs41_superblock.c
  732. +++ b/daemon/nfs41_superblock.c
  733. @@ -1,6 +1,6 @@
  734.  /* NFSv4.1 client for Windows
  735.   * Copyright (C) 2012 The Regents of the University of Michigan
  736. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  737. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  738.   *
  739.   * Olga Kornievskaia <aglo@umich.edu>
  740.   * Casey Bodley <cbodley@umich.edu>
  741. @@ -303,8 +303,12 @@ void nfs41_superblock_fs_attributes(
  742.          FsAttrs->FileSystemAttributes |= FILE_SUPPORTS_HARD_LINKS;
  743.      if (superblock->symlink_support)
  744.          FsAttrs->FileSystemAttributes |= FILE_SUPPORTS_REPARSE_POINTS;
  745. -    if (superblock->ea_support)
  746. +    if (superblock->ea_support) {
  747.          FsAttrs->FileSystemAttributes |= FILE_SUPPORTS_EXTENDED_ATTRIBUTES;
  748. +#ifdef NFS41_WINSTREAMS_SUPPORT
  749. +        FsAttrs->FileSystemAttributes |= FILE_NAMED_STREAMS;
  750. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  751. +    }
  752.      if (superblock->case_preserving)
  753.          FsAttrs->FileSystemAttributes |= FILE_CASE_PRESERVED_NAMES;
  754.      if (!superblock->case_insensitive)
  755. diff --git a/daemon/open.c b/daemon/open.c
  756. index b5e72c7..8af1fd6 100644
  757. --- a/daemon/open.c
  758. +++ b/daemon/open.c
  759. @@ -1,6 +1,6 @@
  760.  /* NFSv4.1 client for Windows
  761.   * Copyright (C) 2012 The Regents of the University of Michigan
  762. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  763. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  764.   *
  765.   * Olga Kornievskaia <aglo@umich.edu>
  766.   * Casey Bodley <cbodley@umich.edu>
  767. @@ -37,6 +37,7 @@
  768.  #include "fileinfoutil.h"
  769.  #include "util.h"
  770.  #include "idmap.h"
  771. +#include "winstreams.h"
  772.  
  773.  static int create_open_state(
  774.      IN const char *path,
  775. @@ -282,8 +283,24 @@ static int open_or_delegate(
  776.  {
  777.      int status;
  778.  
  779. +#ifdef NFS41_WINSTREAMS_SUPPORT
  780. +    /*
  781. +     * FIXME: We get |NFS4ERR_BAD_STATEID| from Solaris 11.4 nfsd if we try
  782. +     * to read a NFS named attribute if the base file has a delegation.
  783. +     * The "why ?" is currently being investigated, for now we just skip the
  784. +     * delegation lookup for NFS named attributes.
  785. +     */
  786. +    if (!is_stream_path_fh(&state->file)) {
  787. +        /* check for existing delegation */
  788. +        status = nfs41_delegate_open(state, create, createattrs, info);
  789. +    }
  790. +    else {
  791. +        status = 1;
  792. +    }
  793. +#else
  794.      /* check for existing delegation */
  795.      status = nfs41_delegate_open(state, create, createattrs, info);
  796. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  797.  
  798.      /* get an open stateid if we have no delegation stateid */
  799.      if (status)
  800. @@ -880,8 +897,12 @@ static int handle_open(void *daemon_context, nfs41_upcall *upcall)
  801.      // first check if windows told us it's a directory
  802.      if (args->create_opts & FILE_DIRECTORY_FILE)
  803.          state->type = NF4DIR;
  804. -    else
  805. -        state->type = NF4REG;
  806. +    else {
  807. +        if (strstr(args->path, ":"))
  808. +            state->type = NF4NAMEDATTR;
  809. +        else
  810. +            state->type = NF4REG;
  811. +    }
  812.  
  813.  #ifdef NFS41_DRIVER_ALLOW_CREATEFILE_ACLS
  814.      if (args->sec_desc_len) {
  815. @@ -1007,20 +1028,26 @@ static int handle_open(void *daemon_context, nfs41_upcall *upcall)
  816.                      goto out_free_state;
  817.                  }
  818.              }
  819. -        } else if (info.type == NF4REG) {
  820. +        } else if ((info.type == NF4REG) || (info.type == NF4NAMEDATTR)) {
  821.              DPRINTF(2, ("handle nfs41_open: FILE\n"));
  822.              if (args->create_opts & FILE_DIRECTORY_FILE) {
  823. -                DPRINTF(1, ("trying to open file '%s' as a directory\n",
  824. -                    state->path.path));
  825. +                DPRINTF(1,
  826. +                    ("trying to open file state->path.path='%s',"
  827. +                    "info.type=%d as a directory\n",
  828. +                    state->path.path, (int)info.type));
  829.                  /*
  830.                   * Notes:
  831.                   * - SMB returns |STATUS_OBJECT_TYPE_MISMATCH|
  832.                   * while NTFS returns |STATUS_NOT_A_DIRECTORY|
  833.                   * - See |map_open_errors()| for the mapping to
  834.                   * |STATUS_*|
  835. +                 * - We do not return |ERROR_DIRECTORY| when opening a Win32
  836. +                 * stream
  837.                   */
  838. -                status = ERROR_DIRECTORY;
  839. -                goto out_free_state;
  840. +                if (is_stream_path(&state->path) == false) {
  841. +                    status = ERROR_DIRECTORY;
  842. +                    goto out_free_state;
  843. +                }
  844.              }
  845.          } else if (info.type == NF4LNK) {
  846.              DPRINTF(2, ("handle nfs41_open: SYMLINK\n"));
  847. @@ -1324,7 +1351,7 @@ supersede_retry:
  848.  
  849.  #ifdef DEBUG_OPEN_SPARSE_FILES
  850.      if ((status == 0) &&
  851. -        (info.type == NF4REG) &&
  852. +        ((info.type == NF4REG) || (info.type == NF4NAMEDATTR)) &&
  853.          (state->session->client->root->supports_nfs42_seek)) {
  854.          //debug_list_sparsefile_holes(state);
  855.          debug_list_sparsefile_datasections(state);
  856. @@ -1524,7 +1551,7 @@ static int handle_close(void *deamon_context, nfs41_upcall *upcall)
  857.      nfs41_open_state *state = upcall->state_ref;
  858.  
  859.      /* return associated file layouts if necessary */
  860. -    if (state->type == NF4REG)
  861. +    if (state->type == NF4REG || state->type == NF4NAMEDATTR)
  862.          pnfs_layout_state_close(state->session, state, args->remove);
  863.  
  864.      if (state->srv_open == args->srv_open)
  865. diff --git a/daemon/upcall.h b/daemon/upcall.h
  866. index 1563c25..01a84ed 100644
  867. --- a/daemon/upcall.h
  868. +++ b/daemon/upcall.h
  869. @@ -1,6 +1,6 @@
  870.  /* NFSv4.1 client for Windows
  871.   * Copyright (C) 2012 The Regents of the University of Michigan
  872. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  873. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  874.   *
  875.   * Olga Kornievskaia <aglo@umich.edu>
  876.   * Casey Bodley <cbodley@umich.edu>
  877. @@ -122,6 +122,10 @@ typedef struct __getattr_upcall_args {
  878.      FILE_STAT_INFORMATION stat_info;
  879.      FILE_STAT_LX_INFORMATION stat_lx_info;
  880.  #endif /* NFS41_DRIVER_WSL_SUPPORT */
  881. +#ifdef NFS41_WINSTREAMS_SUPPORT
  882. +    PFILE_STREAM_INFORMATION stream_info_list;
  883. +    ULONG stream_info_list_size;
  884. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  885.      int query_class;
  886.      int buf_len;
  887.      int query_reply_len;
  888. diff --git a/daemon/winstreams.c b/daemon/winstreams.c
  889. new file mode 100644
  890. index 0000000..0770131
  891. --- /dev/null
  892. +++ b/daemon/winstreams.c
  893. @@ -0,0 +1,595 @@
  894. +/* NFSv4.1 client for Windows
  895. + * Copyright (C) 2024-2026 Roland Mainz <roland.mainz@nrubsig.org>
  896. + *
  897. + * Roland Mainz <roland.mainz@nrubsig.org>
  898. + *
  899. + * This library is free software; you can redistribute it and/or modify it
  900. + * under the terms of the GNU Lesser General Public License as published by
  901. + * the Free Software Foundation; either version 2.1 of the License, or (at
  902. + * your option) any later version.
  903. + *
  904. + * This library is distributed in the hope that it will be useful, but
  905. + * without any warranty; without even the implied warranty of merchantability
  906. + * or fitness for a particular purpose.  See the GNU Lesser General Public
  907. + * License for more details.
  908. + *
  909. + * You should have received a copy of the GNU Lesser General Public License
  910. + * along with this library; if not, write to the Free Software Foundation,
  911. + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  912. + */
  913. +
  914. +#include <stdlib.h>
  915. +#include <stdbool.h>
  916. +
  917. +#include "nfs41_build_features.h"
  918. +#include "winstreams.h"
  919. +#include "from_kernel.h"
  920. +#include "nfs41_ops.h"
  921. +#include "delegation.h"
  922. +#include "upcall.h"
  923. +#include "daemon_debug.h"
  924. +#include "util.h"
  925. +
  926. +#ifdef NFS41_WINSTREAMS_SUPPORT
  927. +
  928. +#define WINSTRLVL 1 /* |dprintf()| level for Windows streams logging */
  929. +
  930. +/*
  931. + * |WIN_NFS4_STREAMS_NAME_PREFIX| - Prefix for Windows streams in NFSv4
  932. + * named attribute namespace
  933. + *
  934. + * We need such a prefix to avoid colliding with other users
  935. + * in the NFSv4 named attribute namespace - for example SUN Microsystrems
  936. + * (Solaris, Illumos, ...) uses "SUNWattr_" as prefix, and setting
  937. + * such attributes can cause data corruption (or in case of
  938. + * "SUNWattr_ro" will fail, because the attribute file is
  939. + * read-only).
  940. + */
  941. +#define WIN_NFS4_STREAMS_NAME_PREFIX "win32.streams."
  942. +#define WIN_NFS4_STREAMS_NAME_PREFIX_LEN (14)
  943. +
  944. +
  945. +static
  946. +int parse_stream_filename_streamname_streamtype(
  947. +    const char *restrict path,
  948. +    char *restrict filename,
  949. +    char *restrict streamname,
  950. +    char *restrict streamtype)
  951. +{
  952. +    const char *sep;
  953. +    const char *base;
  954. +    int colon_count;
  955. +    const char *c1;
  956. +    const char *c2;
  957. +    const char *p;
  958. +    size_t len_prefix;
  959. +    size_t len_sn;
  960. +
  961. +    filename[0]   = '\0';
  962. +    streamname[0] = '\0';
  963. +    streamtype[0] = '\0';
  964. +
  965. +    /* Take the last component after the last backslash */
  966. +    sep  = strrchr(path, '\\');
  967. +    base = sep ? (sep + 1) : path;
  968. +
  969. +    /* Count colons and error out if >= 3 in the final component */
  970. +    colon_count = 0;
  971. +    for (p = base ; *p ; p++) {
  972. +        if (*p == ':') {
  973. +            colon_count++;
  974. +            if (colon_count >= 3)
  975. +                return ERROR_INVALID_NAME;
  976. +        }
  977. +    }
  978. +
  979. +    /* Find first colon (if any) */
  980. +    c1 = strchr(base, ':');
  981. +    if (c1 == NULL) {
  982. +        /* No colon: filename is the entire path */
  983. +        (void)strcpy(filename, path);
  984. +        return NO_ERROR;
  985. +    }
  986. +
  987. +    /* First colon exists: filename before ':' must be non-empty */
  988. +    if (c1 == base)
  989. +        return ERROR_INVALID_NAME;
  990. +
  991. +    /*
  992. +     * One colon case
  993. +     */
  994. +    if (colon_count == 1) {
  995. +        if (*(c1 + 1) == '\0')
  996. +            return ERROR_INVALID_NAME; /* "filename:" => invalid */
  997. +
  998. +        /* filename = full path up to first colon in base */
  999. +        len_prefix = (size_t)(c1 - path);
  1000. +        (void)memcpy(filename, path, len_prefix);
  1001. +        filename[len_prefix] = '\0';
  1002. +
  1003. +        /* streamname after first colon */
  1004. +        (void)strcpy(streamname, c1 + 1);
  1005. +        /* streamtype stays empty */
  1006. +        return NO_ERROR;
  1007. +    }
  1008. +
  1009. +    /*
  1010. +     * Two colons case
  1011. +     */
  1012. +    c2 = strchr(c1 + 1, ':');
  1013. +    if (c2 == NULL) {
  1014. +        /* Should not happen when colon_count == 2 */
  1015. +        return ERROR_INVALID_NAME;
  1016. +    }
  1017. +
  1018. +    if (*(c2 + 1) == '\0')
  1019. +        return ERROR_INVALID_NAME; /* type must be non-empty */
  1020. +
  1021. +    /* filename = full path up to first colon */
  1022. +    len_prefix = (size_t)(c1 - path);
  1023. +    (void)memcpy(filename, path, len_prefix);
  1024. +    filename[len_prefix] = '\0';
  1025. +
  1026. +    /* streamname = [c1+1, c2] (may be empty, e.g., "filename::$DATA") */
  1027. +    len_sn = (size_t)(c2 - (c1 + 1));
  1028. +    (void)memcpy(streamname, c1 + 1, len_sn);
  1029. +    streamname[len_sn] = '\0';
  1030. +
  1031. +    /* streamtype = (c2+1 .. end) */
  1032. +    (void)strcpy(streamtype, c2 + 1);
  1033. +
  1034. +    return NO_ERROR;
  1035. +}
  1036. +
  1037. +int parse_win32stream_name(
  1038. +    IN const char *restrict path,
  1039. +    OUT bool *restrict is_stream,
  1040. +    OUT char *restrict base_name,
  1041. +    OUT char *restrict stream_name)
  1042. +{
  1043. +    int status;
  1044. +    char filenamebuff[NFS41_MAX_PATH_LEN+1];
  1045. +    char streamnamebuff[NFS41_MAX_COMPONENT_LEN+1];
  1046. +    /* |streamtypebuff| must include space for prefix+suffix */
  1047. +    char streamtypebuff[NFS41_MAX_COMPONENT_LEN+1+128];
  1048. +    char *p;
  1049. +
  1050. +    status = parse_stream_filename_streamname_streamtype(path,
  1051. +        filenamebuff, streamnamebuff, streamtypebuff);
  1052. +    if (status) {
  1053. +        eprintf("parse_win32stream_name: "
  1054. +            "parsing for path='%s' failed, status=%d\n",
  1055. +            path, status);
  1056. +        return status;
  1057. +    }
  1058. +
  1059. +    DPRINTF(WINSTRLVL,
  1060. +        ("parse_win32stream_name: "
  1061. +        "parse_stream_filename_streamname_streamtype(path='%s') returned "
  1062. +        "filenamebuff='%s', streamnamebuff='%s', streamtypebuff='%s'\n",
  1063. +        path, filenamebuff, streamnamebuff, streamtypebuff));
  1064. +
  1065. +    if ((streamnamebuff[0] == '\0') && (streamtypebuff[0] == '\0')) {
  1066. +        return ERROR_INVALID_NAME;
  1067. +    }
  1068. +
  1069. +    /* We do not support any stream types except "$DATA" (yet) */
  1070. +    if ((streamtypebuff[0] != '\0') &&
  1071. +        ((_stricmp(streamtypebuff, "$DATA") != 0))) {
  1072. +        eprintf("parse_win32stream_name: "
  1073. +            "Unsupported stream type, path='%s', "
  1074. +            "stream='%s', streamtype='%s'\n",
  1075. +            path,
  1076. +            streamnamebuff,
  1077. +            streamtypebuff);
  1078. +        return ERROR_INVALID_NAME;
  1079. +    }
  1080. +
  1081. +    /* "foo::$DATA" refers to "foo" */
  1082. +    if (streamnamebuff[0] == '\0') {
  1083. +        *is_stream = true;
  1084. +        (void)strcpy(base_name, filenamebuff);
  1085. +        stream_name[0] = '\0';
  1086. +        return NO_ERROR;
  1087. +    }
  1088. +
  1089. +    /*
  1090. +     * If we have a stream name, then add our NFS attr prefix for Windows
  1091. +     * streams, and ":$DATA" as suffix
  1092. +     */
  1093. +    *is_stream = true;
  1094. +    (void)strcpy(base_name, filenamebuff);
  1095. +    p = stpcpy(stream_name, WIN_NFS4_STREAMS_NAME_PREFIX);
  1096. +    p = stpcpy(p, streamnamebuff);
  1097. +    (void)stpcpy(p, ":$DATA");
  1098. +
  1099. +    return NO_ERROR;
  1100. +}
  1101. +
  1102. +#define READDIR_LEN_INITIAL 8192
  1103. +#define READDIR_LEN_MIN 2048
  1104. +
  1105. +/* call readdir repeatedly to get a complete list of entries */
  1106. +static
  1107. +int read_entire_dir(
  1108. +    IN nfs41_session *session,
  1109. +    IN nfs41_path_fh *dir,
  1110. +    OUT unsigned char **restrict buffer_out,
  1111. +    OUT uint32_t *restrict length_out)
  1112. +{
  1113. +    nfs41_readdir_cookie cookie = { 0 };
  1114. +    nfs41_readdir_entry *last_entry;
  1115. +    unsigned char *buffer;
  1116. +    uint32_t buffer_len, len, total_len;
  1117. +    bool_t eof;
  1118. +    int status = NO_ERROR;
  1119. +    /* Attributes for stream */
  1120. +    bitmap4 attr_request = {
  1121. +        .count = 2,
  1122. +        .arr = {
  1123. +            [0] = FATTR4_WORD0_TYPE | FATTR4_WORD0_CHANGE |
  1124. +                FATTR4_WORD0_SIZE | FATTR4_WORD0_FSID |
  1125. +                FATTR4_WORD0_FILEID,
  1126. +            [1] = FATTR4_WORD1_SPACE_USED,
  1127. +            [2] = 0
  1128. +        }
  1129. +    };
  1130. +
  1131. +    /* allocate the buffer for readdir entries */
  1132. +    buffer_len = READDIR_LEN_INITIAL;
  1133. +    buffer = calloc(1, buffer_len);
  1134. +    if (buffer == NULL) {
  1135. +        status = GetLastError();
  1136. +        goto out;
  1137. +    }
  1138. +
  1139. +    last_entry = NULL;
  1140. +    total_len = 0;
  1141. +    eof = FALSE;
  1142. +
  1143. +    while (!eof) {
  1144. +        len = buffer_len - total_len;
  1145. +        if (len < READDIR_LEN_MIN) {
  1146. +            const ptrdiff_t diff = (unsigned char*)last_entry - buffer;
  1147. +            /* realloc the buffer to fit more entries */
  1148. +            unsigned char *tmp = realloc(buffer, (size_t)buffer_len * 2L);
  1149. +            if (tmp == NULL) {
  1150. +                status = GetLastError();
  1151. +                goto out_free;
  1152. +            }
  1153. +
  1154. +            if (last_entry) /* Fix last_entry pointer */
  1155. +                last_entry = (nfs41_readdir_entry *)(tmp + diff);
  1156. +            buffer = tmp;
  1157. +            buffer_len *= 2;
  1158. +            len = buffer_len - total_len;
  1159. +        }
  1160. +
  1161. +        /* Fetch the next group of entries */
  1162. +        status = nfs41_readdir(session, dir, &attr_request,
  1163. +            &cookie, buffer + total_len, &len, &eof);
  1164. +        if (status)
  1165. +            goto out_free;
  1166. +
  1167. +        if (last_entry == NULL) {
  1168. +            /* Initialize last_entry to the front of the list */
  1169. +            last_entry = (nfs41_readdir_entry *)(buffer + total_len);
  1170. +        } else if (len) {
  1171. +            /* Link the previous list to the new one */
  1172. +            last_entry->next_entry_offset = (uint32_t)FIELD_OFFSET(
  1173. +                nfs41_readdir_entry, name) + last_entry->name_len;
  1174. +        }
  1175. +
  1176. +        /* Find the new last entry */
  1177. +        while (last_entry->next_entry_offset) {
  1178. +            last_entry = (nfs41_readdir_entry*)((char*)last_entry +
  1179. +                last_entry->next_entry_offset);
  1180. +        }
  1181. +
  1182. +        cookie.cookie = last_entry->cookie;
  1183. +        total_len += len;
  1184. +    }
  1185. +
  1186. +    *buffer_out = buffer;
  1187. +    *length_out = total_len;
  1188. +out:
  1189. +    return status;
  1190. +
  1191. +out_free:
  1192. +    free(buffer);
  1193. +    goto out;
  1194. +}
  1195. +
  1196. +#define ALIGNED_STREAMINFOSIZE(namebytelen) \
  1197. +    (align8(sizeof(FILE_STREAM_INFORMATION) + (namebytelen)))
  1198. +
  1199. +static
  1200. +uint32_t calculate_stream_list_length(
  1201. +    IN const nfs41_component *restrict basefile_name,
  1202. +    IN const unsigned char *restrict position,
  1203. +    IN uint32_t remaining)
  1204. +{
  1205. +    const nfs41_readdir_entry *entry;
  1206. +    uint32_t length = 0;
  1207. +
  1208. +    /*
  1209. +     * We always have to add a dummy "::$DATA" entry for the default stream!
  1210. +     *
  1211. +     * (|FILE_STREAM_INFORMATION.StreamName| = L"::$DATA" == 8*sizeof(wchar_t))
  1212. +     */
  1213. +    length += ALIGNED_STREAMINFOSIZE(8*sizeof(wchar_t));
  1214. +
  1215. +    /*
  1216. +     * Enumerate streams
  1217. +     */
  1218. +    while (remaining) {
  1219. +        entry = (const nfs41_readdir_entry *)position;
  1220. +
  1221. +        if ((entry->name_len > WIN_NFS4_STREAMS_NAME_PREFIX_LEN) &&
  1222. +            (memcmp(entry->name,
  1223. +                WIN_NFS4_STREAMS_NAME_PREFIX,
  1224. +                WIN_NFS4_STREAMS_NAME_PREFIX_LEN) == 0)) {
  1225. +            char utf8streamname[NFS41_MAX_COMPONENT_LEN+1];
  1226. +            int wcstreamname_len;
  1227. +
  1228. +            (void)snprintf(utf8streamname, sizeof(utf8streamname),
  1229. +                "%.*s:%.*s",
  1230. +                (int)basefile_name->len,
  1231. +                basefile_name->name,
  1232. +                (int)(entry->name_len - WIN_NFS4_STREAMS_NAME_PREFIX_LEN),
  1233. +                &entry->name[WIN_NFS4_STREAMS_NAME_PREFIX_LEN]);
  1234. +
  1235. +            wcstreamname_len = MultiByteToWideChar(CP_UTF8,
  1236. +                MB_ERR_INVALID_CHARS,
  1237. +                utf8streamname,
  1238. +                -1,
  1239. +                NULL,
  1240. +                0);
  1241. +            if (wcstreamname_len <= 0) {
  1242. +                eprintf("calculate_stream_list_length: "
  1243. +                    "Cannot convert utf8streamname='%s' to widechar\n",
  1244. +                    utf8streamname);
  1245. +                goto next_readdir_entry;
  1246. +            }
  1247. +
  1248. +            length +=
  1249. +                ALIGNED_STREAMINFOSIZE(wcstreamname_len*sizeof(WCHAR));
  1250. +        }
  1251. +
  1252. +next_readdir_entry:
  1253. +        if (entry->next_entry_offset == 0)
  1254. +            break;
  1255. +
  1256. +        position += entry->next_entry_offset;
  1257. +        remaining -= entry->next_entry_offset;
  1258. +    }
  1259. +    return length;
  1260. +}
  1261. +
  1262. +static
  1263. +void populate_stream_list(
  1264. +    IN const nfs41_component *restrict basefile_name,
  1265. +    IN const nfs41_file_info *restrict basefile_info,
  1266. +    IN const unsigned char *restrict position,
  1267. +    OUT FILE_STREAM_INFORMATION *restrict stream_list)
  1268. +{
  1269. +    const nfs41_readdir_entry *entry;
  1270. +    PFILE_STREAM_INFORMATION stream = stream_list;
  1271. +    PFILE_STREAM_INFORMATION last_win_stream = NULL;
  1272. +    bool is_win_stream;
  1273. +
  1274. +    /*
  1275. +     * We always have to add a dummy "::$DATA" entry for the default stream!
  1276. +     */
  1277. +    FILE_STREAM_INFORMATION base_stream = {
  1278. +        .NextEntryOffset = 0,
  1279. +        /* "::$DATA" == 8*sizeof(wchar_t) */
  1280. +        .StreamNameLength = 8*sizeof(wchar_t),
  1281. +        .StreamSize.QuadPart = basefile_info->size,
  1282. +        .StreamAllocationSize.QuadPart = basefile_info->space_used
  1283. +    };
  1284. +    (void)memcpy(stream, &base_stream, sizeof(base_stream));
  1285. +    (void)memcpy(stream->StreamName, L"::$DATA", 8*sizeof(wchar_t));
  1286. +    stream->NextEntryOffset =
  1287. +        ALIGNED_STREAMINFOSIZE(stream->StreamNameLength);
  1288. +    last_win_stream = stream;
  1289. +    stream = (PFILE_STREAM_INFORMATION)STREAMINFO_NEXT_ENTRY(stream);
  1290. +
  1291. +    /*
  1292. +     * Enumerate streams
  1293. +     */
  1294. +    for (;;) {
  1295. +        entry = (const nfs41_readdir_entry *)position;
  1296. +
  1297. +        if ((entry->name_len > WIN_NFS4_STREAMS_NAME_PREFIX_LEN) &&
  1298. +            (memcmp(entry->name,
  1299. +                WIN_NFS4_STREAMS_NAME_PREFIX,
  1300. +                WIN_NFS4_STREAMS_NAME_PREFIX_LEN) == 0)) {
  1301. +            is_win_stream = true;
  1302. +        }
  1303. +        else {
  1304. +            is_win_stream = false;
  1305. +        }
  1306. +
  1307. +        if (is_win_stream) {
  1308. +            char utf8streamname[NFS41_MAX_COMPONENT_LEN+1];
  1309. +            int wcstreamname_len;
  1310. +
  1311. +            (void)snprintf(utf8streamname, sizeof(utf8streamname),
  1312. +                "%.*s:%.*s",
  1313. +                (int)basefile_name->len,
  1314. +                basefile_name->name,
  1315. +                (int)(entry->name_len - WIN_NFS4_STREAMS_NAME_PREFIX_LEN),
  1316. +                &entry->name[WIN_NFS4_STREAMS_NAME_PREFIX_LEN]);
  1317. +
  1318. +            wcstreamname_len = MultiByteToWideChar(CP_UTF8,
  1319. +                MB_ERR_INVALID_CHARS,
  1320. +                utf8streamname,
  1321. +                -1,
  1322. +                stream->StreamName,
  1323. +                NFS41_MAX_COMPONENT_LEN);
  1324. +            if (wcstreamname_len <= 0) {
  1325. +                eprintf("populate_stream_list: "
  1326. +                    "Cannot convert utf8streamname='%s' to widechar\n",
  1327. +                    utf8streamname);
  1328. +                goto next_readdir_entry;
  1329. +            }
  1330. +
  1331. +            stream->StreamNameLength = wcstreamname_len*sizeof(WCHAR);
  1332. +
  1333. +            EASSERT(bitmap_isset(&entry->attr_info.attrmask, 0,
  1334. +                FATTR4_WORD0_SIZE));
  1335. +            EASSERT(bitmap_isset(&entry->attr_info.attrmask, 1,
  1336. +                FATTR4_WORD1_SPACE_USED));
  1337. +            stream->StreamSize.QuadPart = entry->attr_info.size;
  1338. +            stream->StreamAllocationSize.QuadPart = entry->attr_info.space_used;
  1339. +
  1340. +            DPRINTF(WINSTRLVL,
  1341. +                ("populate_streams_list: adding stream "
  1342. +                    "entry->(name='%.*s' name_len=%d) "
  1343. +                    "stream->(StreamName='%.*ls', StreamNameLength=%d, "
  1344. +                    "StreamSize=%lld, StreamAllocationSize=%lld)\n",
  1345. +                    (int)entry->name_len,
  1346. +                    entry->name,
  1347. +                    (int)entry->name_len,
  1348. +                    (int)(stream->StreamNameLength/sizeof(WCHAR)),
  1349. +                    stream->StreamName,
  1350. +                    (int)stream->StreamNameLength,
  1351. +                    (long long)stream->StreamSize.QuadPart,
  1352. +                    (long long)stream->StreamAllocationSize.QuadPart));
  1353. +            last_win_stream = stream;
  1354. +        }
  1355. +
  1356. +next_readdir_entry:
  1357. +        if (entry->next_entry_offset == 0) {
  1358. +            (last_win_stream?last_win_stream:stream)->NextEntryOffset = 0;
  1359. +            break;
  1360. +        }
  1361. +
  1362. +        if (is_win_stream) {
  1363. +            stream->NextEntryOffset =
  1364. +                ALIGNED_STREAMINFOSIZE(stream->StreamNameLength);
  1365. +            stream = (PFILE_STREAM_INFORMATION)STREAMINFO_NEXT_ENTRY(stream);
  1366. +        }
  1367. +
  1368. +        position += entry->next_entry_offset;
  1369. +    }
  1370. +}
  1371. +
  1372. +static
  1373. +int get_stream_list(
  1374. +    IN OUT nfs41_open_state *state,
  1375. +    IN nfs41_path_fh *streamfile,
  1376. +    IN const nfs41_file_info *basefile_info,
  1377. +    OUT PFILE_STREAM_INFORMATION *restrict streamlist_out,
  1378. +    OUT ULONG *streamlist_out_size)
  1379. +{
  1380. +    unsigned char *entry_list;
  1381. +    PFILE_STREAM_INFORMATION stream_list;
  1382. +    uint32_t entry_len, stream_list_size;
  1383. +    int status = NO_ERROR;
  1384. +
  1385. +    /* read the entire directory into a |nfs41_readdir_entry| buffer */
  1386. +    status = read_entire_dir(state->session, streamfile,
  1387. +        &entry_list, &entry_len);
  1388. +    if (status)
  1389. +        goto out;
  1390. +
  1391. +    stream_list_size = calculate_stream_list_length(&streamfile->name,
  1392. +        entry_list, entry_len);
  1393. +    if (stream_list_size == 0) {
  1394. +        *streamlist_out = NULL;
  1395. +        *streamlist_out_size = 0UL;
  1396. +        goto out_free;
  1397. +    }
  1398. +    stream_list = calloc(1, stream_list_size);
  1399. +    if (stream_list == NULL) {
  1400. +        status = GetLastError();
  1401. +        goto out_free;
  1402. +    }
  1403. +
  1404. +    populate_stream_list(&streamfile->name, basefile_info,
  1405. +        entry_list, stream_list);
  1406. +
  1407. +    DPRINTF(WINSTRLVL,
  1408. +        ("get_stream_list: stream_list=%p, stream_list_size=%ld\n",
  1409. +        stream_list, (long)stream_list_size));
  1410. +    *streamlist_out = stream_list;
  1411. +    *streamlist_out_size = stream_list_size;
  1412. +out_free:
  1413. +    free(entry_list); /* allocated by |read_entire_dir()| */
  1414. +out:
  1415. +    return status;
  1416. +}
  1417. +
  1418. +int get_streaminformation(
  1419. +    IN OUT nfs41_open_state *state,
  1420. +    IN const nfs41_file_info *basefile_info,
  1421. +    OUT FILE_STREAM_INFORMATION *restrict *restrict streamlist_out,
  1422. +    OUT ULONG *streamlist_out_size)
  1423. +{
  1424. +    int status;
  1425. +    nfs41_path_fh parent = { 0 };
  1426. +
  1427. +    if (!state->file.fh.superblock->ea_support) {
  1428. +        return ERROR_NOT_SUPPORTED;
  1429. +    }
  1430. +
  1431. +    EASSERT_MSG((basefile_info->type != NF4ATTRDIR),
  1432. +        ("state->file.name='%.*s' is a NF4ATTRDIR\n",
  1433. +            (int)state->file.name.len, state->file.name.name));
  1434. +
  1435. +    /* |FileStreamInformation| for symlinks is not supported */
  1436. +    if (basefile_info->type == NF4LNK) {
  1437. +        return ERROR_NOT_SUPPORTED;
  1438. +    }
  1439. +
  1440. +    /*
  1441. +     * FIXME: |FileStreamInformation| for |NF4NAMEDATTR| is not supported
  1442. +     * (yet), the expectation is that doing this for stream "abc:str1" will
  1443. +     * return all streams for "abc"
  1444. +     */
  1445. +    if (basefile_info->type == NF4NAMEDATTR) {
  1446. +        DPRINTF(0,
  1447. +            ("get_streaminformation(name='%.*s'): "
  1448. +            "stream into for NF4NAMEDATTR not implemented yet\n",
  1449. +            (int)state->file.name.len, state->file.name.name));
  1450. +        return ERROR_NOT_SUPPORTED;
  1451. +    }
  1452. +
  1453. +    status = nfs41_rpc_openattr(state->session, &state->file, FALSE,
  1454. +        &parent.fh);
  1455. +
  1456. +    /* No named attribute directory ? */
  1457. +    if (status == NFS4ERR_NOENT) {
  1458. +        /* FIXME: We should return a default "file::$DATA" entry */
  1459. +        DPRINTF(0,
  1460. +            ("get_streaminformation(name='%.*s'): "
  1461. +            "no named attribute directory\n",
  1462. +            (int)state->file.name.len, state->file.name.name));
  1463. +    } else if (status) {
  1464. +        eprintf("get_streaminformation(name='%.*s'): "
  1465. +            "nfs41_rpc_openattr() failed with '%s'\n",
  1466. +            (int)state->file.name.len, state->file.name.name,
  1467. +            nfs_error_string(status));
  1468. +        status = nfs_to_windows_error(status, ERROR_BAD_NET_RESP);
  1469. +        goto out;
  1470. +    }
  1471. +
  1472. +    status = get_stream_list(state,
  1473. +        &parent,
  1474. +        basefile_info,
  1475. +        streamlist_out,
  1476. +        streamlist_out_size);
  1477. +
  1478. +out:
  1479. +    return status;
  1480. +}
  1481. +
  1482. +void free_streaminformation(
  1483. +    IN FILE_STREAM_INFORMATION *restrict streamlist)
  1484. +{
  1485. +    free(streamlist);
  1486. +}
  1487. +
  1488. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  1489. diff --git a/daemon/winstreams.h b/daemon/winstreams.h
  1490. new file mode 100644
  1491. index 0000000..c7a021c
  1492. --- /dev/null
  1493. +++ b/daemon/winstreams.h
  1494. @@ -0,0 +1,76 @@
  1495. +/* NFSv4.1 client for Windows
  1496. + * Copyright (C) 2024-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1497. + *
  1498. + * Roland Mainz <roland.mainz@nrubsig.org>
  1499. + *
  1500. + * This library is free software; you can redistribute it and/or modify it
  1501. + * under the terms of the GNU Lesser General Public License as published by
  1502. + * the Free Software Foundation; either version 2.1 of the License, or (at
  1503. + * your option) any later version.
  1504. + *
  1505. + * This library is distributed in the hope that it will be useful, but
  1506. + * without any warranty; without even the implied warranty of merchantability
  1507. + * or fitness for a particular purpose.  See the GNU Lesser General Public
  1508. + * License for more details.
  1509. + *
  1510. + * You should have received a copy of the GNU Lesser General Public License
  1511. + * along with this library; if not, write to the Free Software Foundation,
  1512. + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  1513. + */
  1514. +
  1515. +#ifndef __NFS41_DAEMON_WINSTREAMS_H__
  1516. +#define __NFS41_DAEMON_WINSTREAMS_H__ 1
  1517. +
  1518. +#include <stdlib.h>
  1519. +#include <stdbool.h>
  1520. +
  1521. +#include "nfs41_build_features.h"
  1522. +#include "nfs41_types.h"
  1523. +#include "from_kernel.h"
  1524. +
  1525. +#define STREAMINFO_NEXT_ENTRY(str) ((PBYTE)(str) + (str)->NextEntryOffset)
  1526. +
  1527. +static __inline
  1528. +bool is_stream_path(const nfs41_abs_path *restrict path)
  1529. +{
  1530. +    if (memchr(path->path, ':', path->len) != NULL)
  1531. +        return true;
  1532. +    return false;
  1533. +}
  1534. +
  1535. +static __inline
  1536. +bool is_stream_path_fh(const nfs41_path_fh *restrict path)
  1537. +{
  1538. +    if (memchr(path->name.name, ':', path->name.len) != NULL)
  1539. +        return true;
  1540. +    return false;
  1541. +}
  1542. +
  1543. +static __inline
  1544. +bool is_stream_component(const nfs41_component *restrict comp)
  1545. +{
  1546. +    if (memchr(comp->name, ':', comp->len) != NULL)
  1547. +        return true;
  1548. +    return false;
  1549. +}
  1550. +
  1551. +#ifdef NFS41_WINSTREAMS_SUPPORT
  1552. +
  1553. +int parse_win32stream_name(
  1554. +    IN const char *restrict path,
  1555. +    OUT bool *restrict is_stream,
  1556. +    OUT char *restrict base_name,
  1557. +    OUT char *restrict stream_name);
  1558. +
  1559. +typedef struct __nfs41_open_state nfs41_open_state;
  1560. +
  1561. +int get_streaminformation(
  1562. +    IN OUT nfs41_open_state *state,
  1563. +    IN const nfs41_file_info *basefile_info,
  1564. +    OUT FILE_STREAM_INFORMATION *restrict *restrict streamlist_out,
  1565. +    OUT ULONG *streamlist_out_size);
  1566. +void free_streaminformation(
  1567. +    IN FILE_STREAM_INFORMATION *restrict streamlist);
  1568. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  1569. +
  1570. +#endif /* !__NFS41_DAEMON_WINSTREAMS_H__ */
  1571. diff --git a/include/from_kernel.h b/include/from_kernel.h
  1572. index b9640a1..c0a24b3 100644
  1573. --- a/include/from_kernel.h
  1574. +++ b/include/from_kernel.h
  1575. @@ -1,6 +1,6 @@
  1576.  /* NFSv4.1 client for Windows
  1577.   * Copyright (C) 2012 The Regents of the University of Michigan
  1578. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1579. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1580.   *
  1581.   * Olga Kornievskaia <aglo@umich.edu>
  1582.   * Casey Bodley <cbodley@umich.edu>
  1583. @@ -191,6 +191,14 @@ typedef struct _FILE_RENAME_INFORMATION {
  1584.      WCHAR FileName[1];
  1585.  } FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION;
  1586.  
  1587. +typedef struct _FILE_STREAM_INFORMATION {
  1588. +    ULONG NextEntryOffset;
  1589. +    ULONG StreamNameLength;
  1590. +    LARGE_INTEGER StreamSize;
  1591. +    LARGE_INTEGER StreamAllocationSize;
  1592. +    WCHAR StreamName[1];
  1593. +} FILE_STREAM_INFORMATION, *PFILE_STREAM_INFORMATION;
  1594. +
  1595.  /* FileAttributeTagInformation==35 */
  1596.  typedef struct _FILE_ATTRIBUTE_TAG_INFORMATION {
  1597.      ULONG FileAttributes;
  1598. diff --git a/nfs41_build_features.h b/nfs41_build_features.h
  1599. index fb744f5..227008f 100644
  1600. --- a/nfs41_build_features.h
  1601. +++ b/nfs41_build_features.h
  1602. @@ -1,6 +1,6 @@
  1603.  /* NFSv4.1 client for Windows
  1604.   * Copyright (C) 2012 The Regents of the University of Michigan
  1605. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1606. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1607.   *
  1608.   * Olga Kornievskaia <aglo@umich.edu>
  1609.   * Casey Bodley <cbodley@umich.edu>
  1610. @@ -273,4 +273,10 @@
  1611.   */
  1612.  #define NFS41_DRIVER_ALLOW_CREATEFILE_ACLS 1
  1613.  
  1614. +/*
  1615. + * |NFS41_WINSTREAMS_SUPPORT| - Enable Win32 named streams support using
  1616. + * NFSv4.1 named attributes
  1617. + */
  1618. +#define NFS41_WINSTREAMS_SUPPORT 1
  1619. +
  1620.  #endif /* !_NFS41_DRIVER_BUILDFEATURES_ */
  1621. diff --git a/sys/nfs41sys_fileinfo.c b/sys/nfs41sys_fileinfo.c
  1622. index f871915..2945a42 100644
  1623. --- a/sys/nfs41sys_fileinfo.c
  1624. +++ b/sys/nfs41sys_fileinfo.c
  1625. @@ -1,6 +1,6 @@
  1626.  /* NFSv4.1 client for Windows
  1627.   * Copyright (C) 2012 The Regents of the University of Michigan
  1628. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1629. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1630.   *
  1631.   * Olga Kornievskaia <aglo@umich.edu>
  1632.   * Casey Bodley <cbodley@umich.edu>
  1633. @@ -171,8 +171,19 @@ void unmarshal_nfs41_getattr(
  1634.      nfs41_updowncall_entry *cur,
  1635.      const unsigned char *restrict *restrict buf)
  1636.  {
  1637. -    unmarshal_nfs41_attrget(cur,
  1638. -        cur->u.QueryFile.buf, &cur->u.QueryFile.buf_len, buf, FALSE);
  1639. +#ifdef NFS41_WINSTREAMS_SUPPORT
  1640. +    if (cur->u.QueryFile.InfoClass == FileStreamInformation) {
  1641. +        /* FIXME: If we do a partial read, what happens to ChangeTime below ? */
  1642. +        unmarshal_nfs41_attrget(cur,
  1643. +            cur->u.QueryFile.buf, &cur->u.QueryFile.buf_len, buf, TRUE);
  1644. +    }
  1645. +    else
  1646. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  1647. +    {
  1648. +        unmarshal_nfs41_attrget(cur,
  1649. +            cur->u.QueryFile.buf, &cur->u.QueryFile.buf_len, buf, FALSE);
  1650. +    }
  1651. +
  1652.      RtlCopyMemory(&cur->ChangeTime, *buf, sizeof(cur->ChangeTime));
  1653.      *buf += sizeof(cur->ChangeTime);
  1654.  #ifdef DEBUG_MARSHAL_DETAIL
  1655. @@ -188,6 +199,7 @@ NTSTATUS map_queryfile_error(
  1656.      case ERROR_ACCESS_DENIED:       return STATUS_ACCESS_DENIED;
  1657.      case ERROR_NETNAME_DELETED:     return STATUS_NETWORK_NAME_DELETED;
  1658.      case ERROR_INVALID_PARAMETER:   return STATUS_INVALID_PARAMETER;
  1659. +    case ERROR_NOT_SUPPORTED:       return STATUS_NOT_SUPPORTED;
  1660.      case ERROR_INTERNAL_ERROR:      return STATUS_INTERNAL_ERROR;
  1661.      default:
  1662.          print_error("map_queryfile_error: "
  1663. @@ -398,6 +410,9 @@ NTSTATUS nfs41_QueryFileInformation(
  1664.      case FileStatInformation:
  1665.      case FileStatLxInformation:
  1666.  #endif /* NFS41_DRIVER_WSL_SUPPORT */
  1667. +#ifdef NFS41_WINSTREAMS_SUPPORT
  1668. +    case FileStreamInformation:
  1669. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  1670.          break;
  1671.      default:
  1672.          print_error("nfs41_QueryFileInformation: "
  1673. @@ -432,7 +447,23 @@ NTSTATUS nfs41_QueryFileInformation(
  1674.          print_error("nfs41_QueryFileInformation: "
  1675.              "entry->status == STATUS_BUFFER_TOO_SMALL\n");
  1676.          status = STATUS_BUFFER_TOO_SMALL;
  1677. -    } else if (entry->status == STATUS_SUCCESS) {
  1678. +    }
  1679. +    else
  1680. +#ifdef NFS41_WINSTREAMS_SUPPORT
  1681. +    if ((InfoClass == FileStreamInformation) &&
  1682. +        (entry->status == STATUS_BUFFER_OVERFLOW)) {
  1683. +        /*
  1684. +         * |FileStreamInformation| must return |STATUS_BUFFER_OVERFLOW| if
  1685. +         * the buffer is too small to store all data
  1686. +         */
  1687. +        RxContext->InformationToReturn = entry->u.QueryFile.buf_len;
  1688. +        print_error("nfs41_QueryFileInformation: "
  1689. +            "FileStreamInformation: "
  1690. +            "entry->status == STATUS_BUFFER_OVERFLOW\n");
  1691. +        status = STATUS_BUFFER_OVERFLOW;
  1692. +    } else
  1693. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  1694. +    if (entry->status == STATUS_SUCCESS) {
  1695.  #ifdef DEBUG_FILE_QUERY
  1696.          print_error("nfs41_QueryFileInformation: entry->status == STATUS_SUCCESS\n");
  1697.  #endif
  1698. @@ -500,6 +531,9 @@ NTSTATUS nfs41_QueryFileInformation(
  1699.          case FileStatInformation:
  1700.          case FileStatLxInformation:
  1701.  #endif /* NFS41_DRIVER_WSL_SUPPORT */
  1702. +#ifdef NFS41_WINSTREAMS_SUPPORT
  1703. +        case FileStreamInformation:
  1704. +#endif /* NFS41_WINSTREAMS_SUPPORT */
  1705.              break;
  1706.          default:
  1707.              print_error("nfs41_QueryFileInformation: "
  1708. diff --git a/sys/nfs41sys_openclose.c b/sys/nfs41sys_openclose.c
  1709. index 93c95b1..d25d719 100644
  1710. --- a/sys/nfs41sys_openclose.c
  1711. +++ b/sys/nfs41sys_openclose.c
  1712. @@ -1,6 +1,6 @@
  1713.  /* NFSv4.1 client for Windows
  1714.   * Copyright (C) 2012 The Regents of the University of Michigan
  1715. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1716. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1717.   *
  1718.   * Olga Kornievskaia <aglo@umich.edu>
  1719.   * Casey Bodley <cbodley@umich.edu>
  1720. @@ -574,12 +574,6 @@ NTSTATUS check_nfs41_create_args(
  1721.          goto out;
  1722.      }
  1723.  
  1724. -    if (isStream(SrvOpen->pAlreadyPrefixedName)) {
  1725. -        DbgP("nfs41_Create: Streams not supported (yet)\n");
  1726. -        status = STATUS_NOT_SUPPORTED;
  1727. -        goto out;
  1728. -    }
  1729. -
  1730.      if (pVNetRootContext->read_only &&
  1731.              (params->DesiredAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA))) {
  1732.          status = STATUS_MEDIA_WRITE_PROTECTED;
  1733. diff --git a/sys/nfs41sys_updowncall.c b/sys/nfs41sys_updowncall.c
  1734. index bc9093e..15f948c 100644
  1735. --- a/sys/nfs41sys_updowncall.c
  1736. +++ b/sys/nfs41sys_updowncall.c
  1737. @@ -1,6 +1,6 @@
  1738.  /* NFSv4.1 client for Windows
  1739.   * Copyright (C) 2012 The Regents of the University of Michigan
  1740. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1741. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1742.   *
  1743.   * Olga Kornievskaia <aglo@umich.edu>
  1744.   * Casey Bodley <cbodley@umich.edu>
  1745. @@ -799,21 +799,27 @@ NTSTATUS nfs41_downcall(
  1746.  
  1747.          /*
  1748.           * Verify that we really read all bytes send by the userland daemon!
  1749. -         * (|NFS41_SYSOP_VOLUME_QUERY| is exempt from this test, because most
  1750. -         * volume queries allows partial reads from |inbuf| if the caller
  1751. -         * passes a buffer which is too small)
  1752. +         *
  1753. +         * (|NFS41_SYSOP_VOLUME_QUERY| and getting |FileStreamInformation| are
  1754. +         * exempt from this test, because most volume queries and
  1755. +         * |FileStreamInformation| allows partial reads from |inbuf| if the
  1756. +         * caller passes a buffer which is too small)
  1757.           */
  1758.          ULONG bytesread_from_inbuf = (ULONG)(inbuf - inbuf_orig);
  1759.          if ((header_tmp->opcode != NFS41_SYSOP_VOLUME_QUERY) &&
  1760. +            (!((header_tmp->opcode == NFS41_SYSOP_FILE_QUERY) &&
  1761. +                (cur->u.QueryFile.InfoClass == FileStreamInformation))) &&
  1762.              (bytesread_from_inbuf != inbuf_len)) {
  1763.              print_error("nfs41_downcall: ASSERT: '%s' (xid=%lld): "
  1764. -                "(inbuf(=0x%p)-inbuf_orig(=0x%p))(=%ld) != inbuf_len(=%ld)\n",
  1765. +                "(inbuf(=0x%p)-inbuf_orig(=0x%p))(=%ld) != inbuf_len(=%ld), "
  1766. +                "cur->status=%d\n",
  1767.                  opcode2string(header_tmp->opcode),
  1768.                  cur->xid,
  1769.                  inbuf,
  1770.                  inbuf_orig,
  1771.                  (long)bytesread_from_inbuf,
  1772. -                (long)inbuf_len);
  1773. +                (long)inbuf_len,
  1774. +                cur->status);
  1775.              status = STATUS_BUFFER_OVERFLOW;
  1776.          }
  1777.      }
  1778. diff --git a/sys/nfs41sys_util.c b/sys/nfs41sys_util.c
  1779. index 345a4b9..c7dc8a2 100644
  1780. --- a/sys/nfs41sys_util.c
  1781. +++ b/sys/nfs41sys_util.c
  1782. @@ -1,6 +1,6 @@
  1783.  /* NFSv4.1 client for Windows
  1784.   * Copyright (C) 2012 The Regents of the University of Michigan
  1785. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1786. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1787.   *
  1788.   * Olga Kornievskaia <aglo@umich.edu>
  1789.   * Casey Bodley <cbodley@umich.edu>
  1790. @@ -88,19 +88,6 @@ BOOLEAN isFilenameTooLong(
  1791.      return FALSE;
  1792.  }
  1793.  
  1794. -BOOLEAN isStream(
  1795. -    PUNICODE_STRING name)
  1796. -{
  1797. -    LONG i;
  1798. -    PWCH p = name->Buffer;
  1799. -    for (i = 0; i < name->Length / 2; i++) {
  1800. -        if (p[0] == L':') return TRUE;
  1801. -        else if (p[0] == L'\0') return FALSE;
  1802. -        p++;
  1803. -    }
  1804. -    return FALSE;
  1805. -}
  1806. -
  1807.  BOOLEAN is_root_directory(
  1808.      PRX_CONTEXT RxContext)
  1809.  {
  1810. diff --git a/sys/nfs41sys_util.h b/sys/nfs41sys_util.h
  1811. index a003fea..eb4971e 100644
  1812. --- a/sys/nfs41sys_util.h
  1813. +++ b/sys/nfs41sys_util.h
  1814. @@ -1,6 +1,6 @@
  1815.  /* NFSv4.1 client for Windows
  1816.   * Copyright (C) 2012 The Regents of the University of Michigan
  1817. - * Copyright (C) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1818. + * Copyright (C) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1819.   *
  1820.   * Olga Kornievskaia <aglo@umich.edu>
  1821.   * Casey Bodley <cbodley@umich.edu>
  1822. @@ -52,8 +52,6 @@ static INLINE ULONG length_as_utf8(
  1823.  BOOLEAN isFilenameTooLong(
  1824.      PUNICODE_STRING name,
  1825.      PNFS41_V_NET_ROOT_EXTENSION pVNetRootContext);
  1826. -BOOLEAN isStream(
  1827. -    PUNICODE_STRING name);
  1828.  BOOLEAN is_root_directory(
  1829.      PRX_CONTEXT RxContext);
  1830.  NTSTATUS nfs41_ProbeAndLockKernelPages(
  1831. diff --git a/tests/manual_testing.txt b/tests/manual_testing.txt
  1832. index a520bfb..f12d057 100644
  1833. --- a/tests/manual_testing.txt
  1834. +++ b/tests/manual_testing.txt
  1835. @@ -1,5 +1,5 @@
  1836.  #
  1837. -# ms-nfs41-client manual testing sequence, 2025-12-12
  1838. +# ms-nfs41-client manual testing sequence, 2026-01-03
  1839.  #
  1840.  # Draft version, needs to be turned into automated tests
  1841.  # if possible
  1842. @@ -218,6 +218,25 @@ $ rm -f d1 d2 d1.diff ; printf '1\n2\n' >d1 ; cp d1 d2 ; printf '3\n' >> d2 ; di
  1843.  The test should print "# test OK"
  1844.  
  1845.  
  1846. +#
  1847. +# Tests for Win32 streams
  1848. +#
  1849. +
  1850. +# this test should print $'dir_line1\ndir_line2\n'
  1851. +rm -Rf dir1 && mkdir dir1 && cmd /c 'echo dir_line1 >>dir1:dirstr1 & echo dir_line2 >>dir1:dirstr1 & cat <dir1:dirstr1'
  1852. +
  1853. +# these five tests should print $'file1data\nfile1_line1\nfile1_line2\n'
  1854. +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'
  1855. +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"
  1856. +rm -f file1 && echo "file1data" >file1 && cmd /c 'echo file1_line1 >>file1:filestr1 & type file1' && powershell -Command 'Add-Content .\file1 -Stream filestr1 -Value "file1_line2" ; Get-Content -Path .\file1 -Stream filestr1'
  1857. +rm -f file1 && echo "file1data" >file1 && cmd /c 'echo file1_line1 >>file1:filestr1 & type file1' && powershell -Command 'Add-Content .\file1:filestr1 -Value "file1_line2" ; Get-Content -Path .\file1 -Stream filestr1'
  1858. +rm -f file1 && echo "file1data" >file1 && cmd /c 'echo file1_line1 >>file1:filestr1' && powershell -Command 'Add-Content .\file1:filestr1 -Value "file1_line2" ; Get-Content -Path .\file1 -Stream ":DATA" ; Get-Content -Path .\file1 -Stream filestr1'
  1859. +# list all streams for file "file1" (should be "filestr1")
  1860. +powershell -Command 'Get-Item -LiteralPath file1 -Stream * | Select *'
  1861. +
  1862. +# FIXME: tests for stream removal, enumeration and sparse data
  1863. +
  1864. +
  1865.  #
  1866.  # Tests for Cycgwin/UWIN/SFU Nfs3Attr EA-based local uid/gid
  1867.  #
  1868. diff --git a/tests/winfsinfo1/winfsinfo.c b/tests/winfsinfo1/winfsinfo.c
  1869. index edc3255..74ac2b7 100644
  1870. --- a/tests/winfsinfo1/winfsinfo.c
  1871. +++ b/tests/winfsinfo1/winfsinfo.c
  1872. @@ -1,7 +1,7 @@
  1873.  /*
  1874.   * MIT License
  1875.   *
  1876. - * Copyright (c) 2023-2025 Roland Mainz <roland.mainz@nrubsig.org>
  1877. + * Copyright (c) 2023-2026 Roland Mainz <roland.mainz@nrubsig.org>
  1878.   *
  1879.   * Permission is hereby granted, free of charge, to any person obtaining a copy
  1880.   * of this software and associated documentation files (the "Software"), to deal
  1881. @@ -1518,6 +1518,85 @@ done:
  1882.      return res;
  1883.  }
  1884.  
  1885. +static
  1886. +int get_filestreaminfo(const char *progname, const char *filename)
  1887. +{
  1888. +    int res = EXIT_FAILURE;
  1889. +    NTSTATUS status;
  1890. +    IO_STATUS_BLOCK iostatus;
  1891. +    PFILE_STREAM_INFORMATION fsi = NULL;
  1892. +
  1893. +    HANDLE fileHandle = CreateFileA(filename,
  1894. +        GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
  1895. +        FILE_FLAG_BACKUP_SEMANTICS, NULL);
  1896. +    if (fileHandle == INVALID_HANDLE_VALUE) {
  1897. +        (void)fprintf(stderr,
  1898. +            "%s: Error opening file '%s'. Last error was %d.\n",
  1899. +            progname,
  1900. +            filename,
  1901. +            (int)GetLastError());
  1902. +        return EXIT_FAILURE;
  1903. +    }
  1904. +
  1905. +#define MAX_STREAM_INFOS (16)
  1906. +#define FSI_MAXCHARS (256)
  1907. +    fsi = calloc(1,
  1908. +        (sizeof(FILE_STREAM_INFORMATION)+sizeof(wchar_t)*FSI_MAXCHARS)*MAX_STREAM_INFOS);
  1909. +    if (fsi == NULL) {
  1910. +         (void)fprintf(stderr,
  1911. +            "%s: Out of memory.\n",
  1912. +            progname);
  1913. +        return EXIT_FAILURE;
  1914. +    }
  1915. +
  1916. +    status = ZwQueryInformationFile(fileHandle,
  1917. +        &iostatus,
  1918. +        fsi,
  1919. +        ((sizeof(FILE_STREAM_INFORMATION)+sizeof(wchar_t)*FSI_MAXCHARS)*MAX_STREAM_INFOS),
  1920. +        FileStreamInformation);
  1921. +
  1922. +    if (status != STATUS_SUCCESS) {
  1923. +        (void)fprintf(stderr, "%s: ZwQueryInformationFile() "
  1924. +            "error. status==0x%lx.\n",
  1925. +            progname,
  1926. +            (long)status);
  1927. +        res = EXIT_FAILURE;
  1928. +        goto done;
  1929. +    }
  1930. +
  1931. +    int streamindex;
  1932. +    FILE_STREAM_INFORMATION *stream;
  1933. +
  1934. +    (void)printf("(\n");
  1935. +    (void)printf("\tfilename='%s'\n", filename);
  1936. +    (void)printf("\ttypeset -a streams=(\n");
  1937. +    for (stream = fsi, streamindex = 0 ; ; streamindex++) {
  1938. +        (void)printf("\t\t[%d]=(\n", streamindex);
  1939. +        (void)printf("\t\t\tStreamName='%.*ls'\n",
  1940. +            (int)(stream->StreamNameLength/sizeof(WCHAR)),
  1941. +            stream->StreamName);
  1942. +        (void)printf("\t\t\tStreamSize=%lld\n",
  1943. +            (long long)stream->StreamSize.QuadPart);
  1944. +        (void)printf("\t\t\tStreamAllocationSize=%lld\n",
  1945. +            (long long)stream->StreamAllocationSize.QuadPart);
  1946. +        (void)printf("\t\t)\n");
  1947. +
  1948. +        if (stream->NextEntryOffset == 0)
  1949. +            break;
  1950. +
  1951. +        stream = (FILE_STREAM_INFORMATION *)((char *)stream + stream->NextEntryOffset);
  1952. +    }
  1953. +    (void)printf("\t)\n");
  1954. +
  1955. +    (void)printf(")\n");
  1956. +    res = EXIT_SUCCESS;
  1957. +
  1958. +done:
  1959. +    free(fsi);
  1960. +    (void)CloseHandle(fileHandle);
  1961. +    return res;
  1962. +}
  1963. +
  1964.  #define BUFFER_SIZE 2048
  1965.  
  1966.  static
  1967. @@ -1769,6 +1848,7 @@ void usage(void)
  1968.          "fileremoteprotocolinfo|"
  1969.          "fileidinfo|"
  1970.          "filenetworkphysicalnameinfo|"
  1971. +        "filestreaminfo|"
  1972.          "fsctlqueryallocatedranges|"
  1973.          "get_wnetgetresourceinformation|"
  1974.          "get_wnetgetresourceparent"
  1975. @@ -1851,6 +1931,9 @@ int main(int ac, char *av[])
  1976.      else if (!strcmp(subcmd, "filenetworkphysicalnameinfo")) {
  1977.          return get_filenetworkphysicalnameinfo(av[0], av[2]);
  1978.      }
  1979. +    else if (!strcmp(subcmd, "filestreaminfo")) {
  1980. +        return get_filestreaminfo(av[0], av[2]);
  1981. +    }
  1982.      else if (!strcmp(subcmd, "fsctlqueryallocatedranges")) {
  1983.          return fsctlqueryallocatedranges(av[0], av[2]);
  1984.      }
  1985. --
  1986. 2.51.0

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.

Syntax highlighting:

To highlight particular lines, prefix each line with {%HIGHLIGHT}




All content is user-submitted.
The administrators of this site (kpaste.net) are not responsible for their content.
Abuse reports should be emailed to us at