Repository: Unidata/netcdf-c (c)

Issue:
nc_open fails in Windows with network drives

netcdf version: 4.8.0
hdf5 version: 1.8.12
OS: windows 10
Compiler: VS2019 u7.7

This was caught while running some of our tests on netcdf files hosted in a network drive.
nc_open fails when the file is accessed using the network path. However, if I map the network drive, and then try to access the same file, it works fine. 

**I could not reproduce the issue with netcdf v4.7.4 + hdf5 1.8.12.**

As shown in the repro code below, v4.8.0 errors if the file is accessed by the network path
`string FILE_NAME = "\\\\mathworks\\devel\\sandbox\\abaruah\\Features\\NetCDF_New_Features\\temp\\monotonic.nc";`

The output of the program below is
Open status: 2
NCID: -858993460
status after close = -33


But if I map the network drive and then try to access the file, it works
`string FILE_NAME = "Y:\\abaruah\\Features\\NetCDF_New_Features\\temp\\monotonic.nc";`

Open status: 0
NCID: 65536
status after close = 0

```
#include <stdio.h>
#include <string>
#include <netcdf.h>
#include <iostream>
#include <vector>

using namespace std;

int
main()
{
    int status;
    int ncid;
    string FILE_NAME = "\\\\mathworks\\devel\\sandbox\\abaruah\\Features\\NetCDF_New_Features\\temp\\monotonic.nc";
    //string FILE_NAME = "Y:\\abaruah\\Features\\NetCDF_New_Features\\temp\\monotonic.nc";

    status = nc_open(FILE_NAME.c_str(), NC_NOWRITE, &ncid);
    std::cout << "Open status: " << status << std::endl;
    std::cout << "NCID: " << ncid << std::endl;
    status = nc_close(ncid);
    printf("status after close = %d\n", status);


    return 0;
}
```

Pull request description:
Support Windows network paths: \\svc\x\y...
* Fixes #2060 

re: Issue https:\\github.com\Unidata\netcdf-c\issues\2060

The path conversion code forgot to consider the case of
windows network paths of the form \\svc\x\y...

I have added support for it, but I can't really test it
since I do not have access to a network drive.

Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 11f68bc91..b69a8966c 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -7,6 +7,7 @@ This file contains a high-level description of this package's evolution. Release
 
 ## 4.8.1 - TBD
 
+* [Enhancement] Support windows network paths (e.g. \\svc\...). See [Github #2065](https://github.com/Unidata/netcdf-c/issues/2065).
 * [Enhancement] Convert to a new representation of the NCZarr meta-data extensions: version 2. Read-only backward compatibility is provided. See [Github #2032](https://github.com/Unidata/netcdf-c/issues/2032).
 * [Bug Fix] Fix dimension_separator bug in libnczarr. See [Github #2035](https://github.com/Unidata/netcdf-c/issues/2035).
 * [Bug Fix] Fix bugs in libdap4. See [Github #2005](https://github.com/Unidata/netcdf-c/issues/2005).
diff --git a/include/ncpathmgr.h b/include/ncpathmgr.h
index c51c9bd20..dd9faea4e 100644
--- a/include/ncpathmgr.h
+++ b/include/ncpathmgr.h
@@ -74,8 +74,12 @@ Assumptions about Input path:
 1. It is a relative or absolute path
 2. It is not a URL
 3. It conforms to the format expected by one of the following:
-       Linux (/x/y/...), Cygwin (/cygdrive/D/...),
-       Windows (D:/...), or MSYS (/D/...), or relative (x/y...)
+       Linux (/x/y/...),
+       Cygwin (/cygdrive/D/...),
+       Windows (D:\...),
+       Windows network path (\\mathworks\...)
+       MSYS (/D/...),
+       or relative (x/y...)
 4. It is encoded in the local platform character set.
    Note that for most systems, this is utf-8. But for Windows,
    the encoding is most likely some form of ANSI code page, probably
@@ -107,14 +111,17 @@ that has some desirable properties:
    the user is responsible for getting this right.
 To this end we choose the linux/cygwin format as our standard canonical form.
 If the path has a windows drive letter, then it is represented
-in the cygwin "/cygdrive/<drive-letter>" form. If it is on *nix* platform,
-then this sequence will never appear and the canonical path will look
-like a standard *nix* path.
+in the cygwin "/cygdrive/<drive-letter>" form.
+If it is a windows network path, then it starts with "//".
+If it is on *nix* platform, then this sequence will never appear
+and the canonical path will look like a standard *nix* path.
 */
 EXTERNL int NCpathcanonical(const char* srcpath, char** canonp);
 
 EXTERNL int NChasdriveletter(const char* path);
 
+EXTERNL int NCisnetworkpath(const char* path);
+
 /* Canonicalize and make absolute by prefixing the current working directory */
 EXTERNL char* NCpathabsolute(const char* name);
 
diff --git a/libdispatch/dpathmgr.c b/libdispatch/dpathmgr.c
index 15c61536c..eb450534e 100644
--- a/libdispatch/dpathmgr.c
+++ b/libdispatch/dpathmgr.c
@@ -59,13 +59,17 @@ Rules:
 2. a leading '/cygdrive/X' will be converted to
    a drive letter X if X is alpha-char.
 3. a leading D:/... is treated as a windows drive letter
+4. a leading // is a windows network path and is converted
+   to a drive letter using the fake drive letter "@".
 5. If any of the above is encountered, then forward slashes
    will be converted to backslashes.
 All other cases are passed thru unchanged
 */
 
 /* Define legal windows drive letters */
-static const char* windrive = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+static const char* windrive = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@";
+
+static const char netdrive = '@';
 
 static const size_t cdlen = 10; /* strlen("/cygdrive/") */
 
@@ -600,6 +604,27 @@ done:
     return hasdl;
 }
 
+EXTERNL int
+NCisnetworkpath(const char* path)
+{
+    int stat = NC_NOERR;
+    int isnp = 0;    
+    struct Path canon = empty;
+
+    if(!pathinitialized) pathinit();     
+
+    if((stat = parsepath(path,&canon))) goto done;
+    if(canon.kind == NCPD_REL) {
+	clearPath(&canon);
+        /* Get the drive letter (if any) from the local wd */
+	canon.drive = wdpath.drive;	
+    }
+    isnp = (canon.drive == netdrive);
+done:
+    clearPath(&canon);
+    return isnp;
+}
+
 /**************************************************/
 /* Utilities */
 
@@ -626,15 +651,27 @@ parsepath(const char* inpath, struct Path* path)
     /* Convert to forward slash */
     for(p=tmp1;*p;p++) {if(*p == '\\') *p = '/';}
 
-    /* parse all paths to 2-parts:
+    /* parse all paths to 2 parts:
 	1. drive letter (optional)
 	2. path after drive letter
     */
 
     len = strlen(tmp1);
 
-    /* 1. look for MSYS path /D/... */
-    if(len >= 2
+    /* 1. look for Windows network path //... */
+    if(len >= 2 && (tmp1[0] == '/') && (tmp1[1] == '/')) {
+	path->drive = netdrive;
+	/* Remainder */
+	if(tmp1[2] == '\0')
+	    path->path = NULL;
+	else
+	    path->path = strdup(tmp1+1); /*keep first '/' */
+	if(path == NULL)
+	    {stat = NC_ENOMEM; goto done;}
+	path->kind = NCPD_WIN;
+    }
+    /* 2. look for MSYS path /D/... */
+    else if(len >= 2
 	&& (tmp1[0] == '/')
 	&& strchr(windrive,tmp1[1]) != NULL
 	&& (tmp1[2] == '/' || tmp1[2] == '\0')) {
@@ -649,7 +686,7 @@ parsepath(const char* inpath, struct Path* path)
 	    {stat = NC_ENOMEM; goto done;}
 	path->kind = NCPD_MSYS;
     }
-    /* 2. Look for leading /cygdrive/D where D is a single-char drive letter */
+    /* 3. Look for leading /cygdrive/D where D is a single-char drive letter */
     else if(len >= (cdlen+1)
 	&& memcmp(tmp1,"/cygdrive/",cdlen)==0
 	&& strchr(windrive,tmp1[cdlen]) != NULL
@@ -666,7 +703,7 @@ parsepath(const char* inpath, struct Path* path)
 	    {stat = NC_ENOMEM; goto done;}
 	path->kind = NCPD_CYGWIN;
     }
-    /* 3. Look for windows path:  D:/... where D is a single-char
+    /* 4. Look for windows path:  D:/... where D is a single-char
           drive letter */
     else if(len >= 2
 	&& strchr(windrive,tmp1[0]) != NULL
@@ -683,14 +720,14 @@ parsepath(const char* inpath, struct Path* path)
 	    {stat = NC_ENOMEM; goto done;}
 	path->kind = NCPD_WIN;
     }
-    /* look for *nix path */
+    /* 5. look for *nix path */
     else if(len >= 1 && tmp1[0] == '/') {
 	/* Assume this is a *nix path */
 	path->drive = 0; /* no drive letter */
 	/* Remainder */
 	path->path = tmp1; tmp1 = NULL;
 	path->kind = NCPD_NIX;	
-    } else {/* Relative path of unknown type */
+    } else {/* 6. Relative path of unknown type */
 	path->kind = NCPD_REL;
 	path->path = tmp1; tmp1 = NULL;
     }
@@ -748,13 +785,17 @@ unparsepath(struct Path* xp, char** pathp)
 	break;
     case NCPD_WIN:
 	if(xp->drive == 0) {xp->drive = wdpath.drive;} /*requires a drive */
-	len = nulllen(xp->path)+2+1;
+	len = nulllen(xp->path)+2+1+1;
 	if((path = (char*)malloc(len))==NULL)
 	    {stat = NC_ENOMEM; goto done;}	
 	path[0] = '\0';
-	sdrive[0] = xp->drive;
-	strlcat(path,sdrive,len);
-	strlcat(path,":",len);
+	if(xp->drive == netdrive)
+	    strlcat(path,"/",len); /* second slash will come from path */
+	else {
+	    sdrive[0] = xp->drive;
+	    strlcat(path,sdrive,len);
+	    strlcat(path,":",len);
+	}
 	if(xp->path)
 	    strlcat(path,xp->path,len);
 	/* Convert forward to back */ 


Test patch:
diff --git a/unit_test/test_pathcvt.c b/unit_test/test_pathcvt.c
index e1725aa63..98d942209 100644
--- a/unit_test/test_pathcvt.c
+++ b/unit_test/test_pathcvt.c
@@ -43,6 +43,12 @@ static Test PATHTESTS[] = {
 #ifndef _WIN32
 /* Test utf8 path */
 {"/海/海",{ "/海/海", "/c/海/海", "/cygdrive/c/海/海",  "c:\\海\\海"}},
+/* Test network path */
+{"//git/netcdf-c/dap4_test",{
+    "/@/git/netcdf-c/dap4_test",
+    "/@/git/netcdf-c/dap4_test",
+    "/cygdrive/@/git/netcdf-c/dap4_test",
+    "\\\\git\\netcdf-c\\dap4_test"}},
 #endif
 {NULL, {NULL, NULL, NULL, NULL}}
 };
@@ -62,11 +68,11 @@ main(int argc, char** argv)
 
     nc_initialize();
 
-    /* Test localkind X path kind */
-    for(k=0;k<NKINDS;k++) {
-	int kind = kinds[k];
-	/* Iterate over the test paths */
-        for(test=PATHTESTS;test->test;test++) {
+    /* Test localkind X path-kind */
+    for(test=PATHTESTS;test->test;test++) {
+        /* Iterate over the test paths */
+        for(k=0;k<NKINDS;k++) {
+	    int kind = kinds[k];
 	    /* Compare output for the localkind */
             if(test->expected[k] == NULL) {
 #ifdef DEBUG