This patch adds a module for determining whether a file (or directory)
is on a local or a remote file system.


2025-11-12  Bruno Haible  <[email protected]>

        file-remote: New module.
        * lib/file-remote.h: New file.
        * lib/file-remote.c: New file.
        * modules/file-remote: New file.

>From 23afd19a4ff4fe9141dadd65d5c2bc1ccbfb9cc6 Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Wed, 12 Nov 2025 13:57:50 +0100
Subject: [PATCH] file-remote: New module.

* lib/file-remote.h: New file.
* lib/file-remote.c: New file.
* modules/file-remote: New file.
---
 ChangeLog           |   7 ++
 lib/file-remote.c   | 287 ++++++++++++++++++++++++++++++++++++++++++++
 lib/file-remote.h   |  37 ++++++
 modules/file-remote |  25 ++++
 4 files changed, 356 insertions(+)
 create mode 100644 lib/file-remote.c
 create mode 100644 lib/file-remote.h
 create mode 100644 modules/file-remote

diff --git a/ChangeLog b/ChangeLog
index 9d6b483bb6..8a9b8d3721 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+2025-11-12  Bruno Haible  <[email protected]>
+
+	file-remote: New module.
+	* lib/file-remote.h: New file.
+	* lib/file-remote.c: New file.
+	* modules/file-remote: New file.
+
 2025-11-11  Bruno Haible  <[email protected]>
 
 	pthread-cond, cond, cnd tests: Fix spurious failure on Linux/SPARC.
diff --git a/lib/file-remote.c b/lib/file-remote.c
new file mode 100644
index 0000000000..bad7ce011d
--- /dev/null
+++ b/lib/file-remote.c
@@ -0,0 +1,287 @@
+/* Determination whether a file is local or remote.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This file is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published
+   by the Free Software Foundation, either version 3 of the License,
+   or (at your option) any later version.
+
+   This file is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#include <config.h>
+
+/* Specification.  */
+#include "file-remote.h"
+
+#include <string.h>
+
+#if defined __linux__ || defined __ANDROID__                /* Linux */
+# include <sys/statfs.h>
+#elif defined __gnu_hurd__                                  /* GNU/Hurd */
+# include <sys/statfs.h>
+# include <hurd/hurd_types.h>
+#elif defined __APPLE__ && defined __MACH__                 /* macOS */
+# include <sys/mount.h>
+#elif defined __FreeBSD__ || defined __DragonFly__ || defined __FreeBSD_kernel__
+                                                     /* FreeBSD, GNU/kFreeBSD */
+# include <sys/mount.h>
+#elif defined __NetBSD__                                    /* NetBSD */
+# include <sys/statvfs.h>
+#elif defined __OpenBSD__                                   /* OpenBSD */
+# include <sys/types.h>
+# include <sys/mount.h>
+#elif defined _AIX                                          /* AIX */
+# include <sys/statvfs.h>
+#elif defined __sun                                         /* Solaris */
+# include <sys/statvfs.h>
+#elif defined __CYGWIN__                                    /* Cygwin */
+# include "cygpath.h"
+#elif defined __HAIKU__                                     /* Haiku */
+# include <fs_info.h>
+# include <errno.h>
+#endif
+
+#if defined _WIN32 || defined __CYGWIN__                    /* Windows */
+# define WIN32_LEAN_AND_MEAN  /* avoid including junk */
+# include <windows.h>
+# include <errno.h>
+/* Don't assume that UNICODE is not defined.  */
+# undef GetFullPathName
+# define GetFullPathName GetFullPathNameA
+# undef GetDriveType
+# define GetDriveType GetDriveTypeA
+
+/* Here, FILE is a file or directory name in native Windows syntax.  */
+static int
+windows_file_is_remote (const char *file)
+{
+  char buf[MAX_PATH];
+  /* Documentation:
+     <https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamea>  */
+  if (! GetFullPathName (file, sizeof (buf), buf, NULL))
+    {
+      switch (GetLastError ())
+        {
+        case ERROR_FILE_NOT_FOUND: /* The last component of file does not exist.  */
+        case ERROR_PATH_NOT_FOUND: /* Some directory component in file does not exist.  */
+        case ERROR_BAD_PATHNAME:   /* file is such as '\\server'.  */
+        case ERROR_BAD_NET_NAME:   /* file is such as '\\server\nonexistentshare'.  */
+        case ERROR_INVALID_NAME:   /* file contains wildcards, misplaced colon, etc.  */
+          errno = ENOENT;
+          break;
+        case ERROR_ACCESS_DENIED:  /* file is such as 'C:\System Volume Information\foo'.  */
+        case ERROR_SHARING_VIOLATION: /* file is such as 'C:\pagefile.sys'.  */
+                                      /* XXX map to EACCES or EPERM? */
+          errno = EACCES;
+          break;
+        default:
+          errno = EINVAL;
+          break;
+        }
+      return -1;
+    }
+  const char *root = buf;
+  if (root[0] == '\\' && root[1] == '\\')
+    {
+      if (root[2] == '?' && root[3] == '\\')
+        {
+          if (strncmp (root + 4, "Volume", 6) == 0)
+            /* '\\?\Volume{GUID}\' designates a local volume.  */
+            return 0;
+          if (strncmp (root + 4, "UNC\\", 4) == 0)
+            /* '\\?\UNC\server\share\' designates a remote mount.  */
+            return 1;
+          root += 4;
+        }
+      else
+        /* '\\server\share\' designates a remote mount.  */
+        return 1;
+    }
+  if (root[0] != '\0' && root[1] == ':')
+    {
+      /* Documentation:
+         <https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetypea>  */
+      UINT type = GetDriveType (root);
+      return type == DRIVE_REMOTE;
+    }
+  /* ROOT does not look like a remote mount.  */
+  return 0;
+}
+
+#endif
+
+int
+file_is_remote (const char *file)
+{
+#if defined __linux__ || defined __ANDROID__                /* Linux */
+  struct statfs fs;
+  if (statfs (file, &fs) < 0)
+    return -1;
+  /* See coreutils/src/fs.h and coreutils/src/fs-is-local.h.  */
+  switch (fs.f_type)
+    {
+    case 0x61636673 /* S_MAGIC_ACFS */:
+    case 0x5346414F /* S_MAGIC_AFS */:
+    case 0x00C36400 /* S_MAGIC_CEPH */:
+    case 0xFF534D42 /* S_MAGIC_CIFS */:
+    case 0x73757245 /* S_MAGIC_CODA */:
+    case 0x19830326 /* S_MAGIC_FHGFS */:
+    case 0x65735546 /* S_MAGIC_FUSE */:
+    case 0x65735543 /* S_MAGIC_FUSECTL */:
+    case 0x01161970 /* S_MAGIC_GFS */:
+    case 0x47504653 /* S_MAGIC_GPFS */:
+    case 0x013111A8 /* S_MAGIC_IBRIX */:
+    case 0x6B414653 /* S_MAGIC_KAFS */:
+    case 0x0BD00BD0 /* S_MAGIC_LUSTRE */:
+    case 0x0000564C /* S_MAGIC_NCP */:
+    case 0x00006969 /* S_MAGIC_NFS */:
+    case 0x6E667364 /* S_MAGIC_NFSD */:
+    case 0x7461636F /* S_MAGIC_OCFS2 */:
+    case 0x794C7630 /* S_MAGIC_OVERLAYFS */:
+    case 0xAAD7AAEA /* S_MAGIC_PANFS */:
+    case 0x50495045 /* S_MAGIC_PIPEFS */:
+    case 0x7C7C6673 /* S_MAGIC_PRL_FS */:
+    case 0x0000517B /* S_MAGIC_SMB */:
+    case 0xFE534D42 /* S_MAGIC_SMB2 */:
+    case 0xBEEFDEAD /* S_MAGIC_SNFS */:
+    case 0x786F4256 /* S_MAGIC_VBOXSF */:
+    case 0xBACBACBC /* S_MAGIC_VMHGFS */:
+    case 0xA501FCF5 /* S_MAGIC_VXFS */:
+      return 1;
+    default:
+      return 0;
+    }
+#elif defined __gnu_hurd__                                  /* GNU/Hurd */
+  /* On this platform, we could equally use 'statvfs', as
+     it's identical to 'statfs'.  */
+  struct statfs fs;
+  if (statfs (file, &fs) < 0)
+    return -1;
+  /* See <hurd/hurd_types.h>.
+     Note: The only ones of these that are currently implemented in hurd.git
+     are nfs and ftpfs.  */
+  switch (fs.f_type)
+    {
+    case FSTYPE_NFS:    /* Network File System ala Sun */
+    case FSTYPE_FTP:    /* Transparent FTP */
+    case FSTYPE_GRFS:   /* GNU Remote File System */
+    case FSTYPE_AFS:    /* Andrew File System 3.xx */
+    case FSTYPE_DFS:    /* Distributed File Sys (OSF) == AFS 4.xx */
+    case FSTYPE_SOCKET: /* io_t that isn't a file but a socket */
+    case FSTYPE_HTTP:   /* Transparent HTTP */
+      return 1;
+    default:
+      return 0;
+    }
+#elif defined __APPLE__ && defined __MACH__                 /* macOS */
+  struct statfs fs;
+  if (statfs (file, &fs) < 0)
+    return -1;
+  return (fs.f_flags & MNT_LOCAL) == 0
+         && !(strcmp (fs.f_fstypename, "fdesc") == 0);
+#elif defined __FreeBSD__ || defined __DragonFly__ || defined __FreeBSD_kernel__
+                                                     /* FreeBSD, GNU/kFreeBSD */
+  struct statfs fs;
+  if (statfs (file, &fs) < 0)
+    return -1;
+  return (fs.f_flags & MNT_LOCAL) == 0
+         && !(strcmp (fs.f_fstypename, "devfs") == 0
+              || strcmp (fs.f_fstypename, "fdescfs") == 0);
+#elif defined __NetBSD__                                    /* NetBSD */
+  struct statvfs fs;
+  if (statvfs1 (file, &fs, ST_NOWAIT) < 0)
+    return -1;
+  return (fs.f_flag & ST_LOCAL) == 0;
+#elif defined __OpenBSD__                                   /* OpenBSD */
+  struct statfs fs;
+  if (statfs (file, &fs) < 0)
+    return -1;
+  return (fs.f_flags & MNT_LOCAL) == 0;
+#elif defined _AIX                                          /* AIX */
+  struct statvfs fs;
+  if (statvfs (file, &fs) < 0)
+    return -1;
+  /* See /etc/vfs.  */
+  return (strcmp (fs.f_basetype, "nfs") == 0
+          || strcmp (fs.f_basetype, "nfs3") == 0
+          || strcmp (fs.f_basetype, "nfs4") == 0
+          || strcmp (fs.f_basetype, "stnfs") == 0
+          || strcmp (fs.f_basetype, "cachefs") == 0);
+#elif defined __sun                                         /* Solaris */
+  struct statvfs fs;
+  if (statvfs (file, &fs) < 0)
+    return -1;
+  /* See /etc/dfs/fstypes.  */
+  return (strcmp (fs.f_basetype, "nfs") == 0
+          || strcmp (fs.f_basetype, "smb") == 0
+          || strcmp (fs.f_basetype, "smbfs") == 0
+          || strcmp (fs.f_basetype, "autofs") == 0);
+#elif defined __CYGWIN__                                    /* Cygwin */
+  /* The 'struct statfs' member 'f_type' does not have the
+     necessary information.  We need to use the native Windows API.  */
+  char *windows_file = cygpath_w (file);
+  int result = windows_file_is_remote (windows_file);
+  free (windows_file);
+  return result;
+#elif defined _WIN32 && !defined __CYGWIN__                 /* Native Windows */
+  return windows_file_is_remote (file);
+#elif defined __HAIKU__                                     /* Haiku */
+  /* Documentation:
+     https://www.haiku-os.org/legacy-docs/bebook/TheStorageKit_Functions.html#dev_for_path
+     out-of-date: This function actually sets errno when it fails.  */
+  dev_t device = dev_for_path (file);
+  if (device < 0)
+    return -1;
+  /* Documentation:
+     https://www.haiku-os.org/legacy-docs/bebook/TheStorageKit_Functions.html#fs_stat_dev
+     https://www.haiku-os.org/legacy-docs/bebook/TheStorageKit_DefinedTypes.html#fs_info
+     This function too actually sets errno when it fails.  */
+  struct fs_info fs;
+  if (fs_stat_dev (device, &fs) != B_OK)
+    return -1;
+  return (strcmp (fs.fsh_name, "userlandfs") == 0
+          || strcmp (fs.fsh_name, "nfs") == 0
+          || strcmp (fs.fsh_name, "netfs") == 0
+          || strcmp (fs.fsh_name, "websearchfs") == 0);
+#else                                                       /* Unknown OS */
+  /* Assume all file systems are local.  */
+  return 0;
+#endif
+}
+
+
+#if TEST
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+/* Test program that prints the result for a file name given on the command
+   line.  */
+int
+main (int argc, char *argv[])
+{
+  int i;
+
+  for (i = 1; i < argc; i++)
+    {
+      const char *file = argv[i];
+      int ret = file_is_remote (file);
+      if (ret == 0)
+        printf ("%s => local\n", file);
+      else if (ret > 0)
+        printf ("%s => remote\n", file);
+      else
+        printf ("%s => error: %s\n", file, strerror (errno));
+    }
+
+  return 0;
+}
+
+#endif /* TEST */
diff --git a/lib/file-remote.h b/lib/file-remote.h
new file mode 100644
index 0000000000..af9b4030c3
--- /dev/null
+++ b/lib/file-remote.h
@@ -0,0 +1,37 @@
+/* Determination whether a file is local or remote.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This file is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published
+   by the Free Software Foundation, either version 3 of the License,
+   or (at your option) any later version.
+
+   This file is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifndef _FILE_REMOTE_H
+#define _FILE_REMOTE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Determines whether a file (or directory) is remote, that is, whether
+   a file system operation on the file, such as mkdir() or open(), can
+   take an unlimited amount of time, due to network issues.
+   Returns
+     - 0, if it is local,
+     - 1, if it is remote,
+     - -1 with errno set, upon error (e.g. if the file is not accessible).  */
+extern int file_is_remote (const char *file);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _FILE_REMOTE_H */
diff --git a/modules/file-remote b/modules/file-remote
new file mode 100644
index 0000000000..6090103934
--- /dev/null
+++ b/modules/file-remote
@@ -0,0 +1,25 @@
+Description:
+Determine whether a file is local or remote.
+
+Files:
+lib/file-remote.h
+lib/file-remote.c
+
+Depends-on:
+cygpath
+
+configure.ac:
+
+Makefile.am:
+lib_SOURCES += file-remote.c
+
+Include:
+"file-remote.h"
+
+Link:
+
+License:
+GPL
+
+Maintainer:
+all
-- 
2.51.0

Reply via email to