Commit 7c6d726834 for openssl.org

commit 7c6d726834d21f6db4180f881b7df1d6f2edd620
Author: Dr. David von Oheimb <dev@ddvo.net>
Date:   Sat Apr 26 14:37:12 2025 +0200

    Factor out ossl_file_stat() from file_store.c:file_open()

    It is also simplified and generalized.

    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    MergeDate: Thu Apr 16 16:52:56 2026
    (Merged from https://github.com/openssl/openssl/pull/27554)

diff --git a/providers/implementations/storemgmt/file_store.c b/providers/implementations/storemgmt/file_store.c
index ee8c9c04e4..2df5406699 100644
--- a/providers/implementations/storemgmt/file_store.c
+++ b/providers/implementations/storemgmt/file_store.c
@@ -10,23 +10,19 @@
 /* This file has quite some overlap with engines/e_loader_attic.c */

 #include <string.h>
-#include <sys/stat.h>
+#include "internal/e_os.h" /* for stat() */
+#include <sys/stat.h> /* for struct stat */
 #include <ctype.h> /* isdigit */
 #include <assert.h>

-#include <openssl/core_dispatch.h>
 #include <openssl/core_names.h>
 #include <openssl/core_object.h>
-#include <openssl/bio.h>
-#include <openssl/err.h>
 #include <openssl/params.h>
-#include <openssl/decoder.h>
 #include <openssl/proverr.h>
 #include <openssl/store.h> /* The OSSL_STORE_INFO type numbers */
 #include "internal/cryptlib.h"
 #include "internal/o_dir.h"
 #include "crypto/decoder.h"
-#include "crypto/ctype.h" /* ossl_isdigit() */
 #include "prov/implementations.h"
 #include "prov/bio.h"
 #include "prov/providercommon.h"
@@ -36,10 +32,6 @@

 DEFINE_STACK_OF(OSSL_STORE_INFO)

-#ifdef _WIN32
-#define stat _stat
-#endif
-
 #ifndef S_ISDIR
 #define S_ISDIR(a) (((a) & S_IFMT) == S_IFDIR)
 #endif
@@ -142,6 +134,94 @@ static struct file_ctx_st *new_file_ctx(int type, const char *uri,
 static OSSL_DECODER_CONSTRUCT file_load_construct;
 static OSSL_DECODER_CLEANUP file_load_cleanup;

+#ifdef _WIN32
+#define OSSL_is_drive_letter(c) (((c) >= 'A' && (c) <= 'Z') || ((c) >= 'a' && (c) <= 'z'))
+#define OSSL_is_abs_drive_prefix(p) (OSSL_is_drive_letter((p)[0]) && (p)[1] == ':' && (p)[2] == '/')
+#endif
+
+/*
+ * uri_file_stat() handles URIs that may be interpreted as a reference to a local file.
+ * It attempts to derive from the given |uri| a file pathname that points to an
+ * existing file. To this end it takes the full |uri| as a filename (which may be
+ * an absolute or relative name, such as file.pem) or takes a postfix of |uri|,
+ * such as path-to-file if |uri| is of the form file:path-to-file
+ * or /path-to-file if |uri| is of the form file://localhost/path-to-file.
+ * The returned pathname is a pointer inside |uri|, or NULL on error.
+ * On success it populates the file stat buffer pointed at by |st|
+ * (unless |st| is NULL) and returns the derived pathname, otherwise NULL.
+ */
+static const char *uri_file_stat(const char *uri, struct stat *st)
+{
+    const char *path = uri, *q;
+    struct stat local_st;
+
+    ERR_set_mark();
+
+    if (st == NULL)
+        st = &local_st;
+
+    /*
+     * First, unless the URI starts with "file://",
+     * try and see if the full URI can be taken as a local file path name.
+     */
+    if (!HAS_CASE_PREFIX(uri, "file://")) {
+        if (stat(path, st) == 0) {
+            ERR_pop_to_mark();
+            return uri;
+        }
+        ERR_raise_data(ERR_LIB_SYS, errno, "calling stat(%s)", path);
+    }
+
+    /* Do a second attempt only if the URI appears to start with the "file" scheme. */
+    if (!CHECK_AND_SKIP_CASE_PREFIX(path, "file:")) {
+        ERR_clear_last_mark();
+        return NULL;
+    }
+
+    /*
+     * Extract the alternative path to check.
+     * There's a special case if the URI also contains an authority,
+     * then the full URI shouldn't be used as a path anywhere.
+     */
+    q = path;
+    if (CHECK_AND_SKIP_CASE_PREFIX(q, "//")) {
+        if (CHECK_AND_SKIP_CASE_PREFIX(q, "localhost/")
+            || CHECK_AND_SKIP_CASE_PREFIX(q, "/")) {
+            /*
+             * In these cases, we step back one char to ensure that the
+             * first slash is preserved, making the path always absolute
+             */
+            path = q - 1;
+#ifdef _WIN32
+        } else if (OSSL_is_abs_drive_prefix(q)) {
+            /* Support also "file://" URIs starting with a Windows drive letter not preceded by an extra '/' */
+            path = q;
+#endif
+        } else {
+            const char *p = strchr(q, '/');
+            size_t len = p == NULL ? strlen(q) : (size_t)(p - q);
+
+            ERR_raise_data(ERR_LIB_OSSL_STORE, OSSL_STORE_R_URI_AUTHORITY_UNSUPPORTED,
+                "%.*s", len, q);
+            ERR_clear_last_mark();
+            return NULL;
+        }
+    }
+#ifdef _WIN32
+    /* Windows "file://" URIs with a drive letter are usually required to have an extra '/' before the drive letter, e.g., "file:///C:/path" */
+    if (path[0] == '/' && OSSL_is_abs_drive_prefix(path + 1))
+        path++; /* Skip past the slash, making the path a normal Windows path */
+#endif
+
+    if (stat(path, st) == 0) {
+        ERR_pop_to_mark();
+        return path;
+    }
+    ERR_raise_data(ERR_LIB_SYS, errno, "calling stat(%s)", path);
+    ERR_clear_last_mark();
+    return NULL;
+}
+
 /*-
  *  Opening / attaching streams and directories
  *  -------------------------------------------
@@ -199,71 +279,11 @@ static void *file_open(void *provctx, const char *uri)
 {
     struct file_ctx_st *ctx = NULL;
     struct stat st;
-    const char *path_data[2];
-    size_t path_data_n = 0, i;
-    const char *path, *p = uri, *q;
+    const char *path = uri_file_stat(uri, &st);
     BIO *bio;

-    ERR_set_mark();
-
-    /*
-     * First step, just take the URI as is.
-     */
-    path_data[path_data_n++] = uri;
-
-    /*
-     * Second step, if the URI appears to start with the "file" scheme,
-     * extract the path and make that the second path to check.
-     * There's a special case if the URI also contains an authority, then
-     * the full URI shouldn't be used as a path anywhere.
-     */
-    if (CHECK_AND_SKIP_CASE_PREFIX(p, "file:")) {
-        q = p;
-        if (CHECK_AND_SKIP_CASE_PREFIX(q, "//")) {
-            path_data_n--; /* Invalidate using the full URI */
-            if (CHECK_AND_SKIP_CASE_PREFIX(q, "localhost/")
-                || CHECK_AND_SKIP_CASE_PREFIX(q, "/")) {
-                /*
-                 * In this case, we step back on char to ensure that the
-                 * first slash is preserved, making the path always absolute
-                 */
-                p = q - 1;
-            } else {
-                ERR_clear_last_mark();
-                ERR_raise(ERR_LIB_PROV, PROV_R_URI_AUTHORITY_UNSUPPORTED);
-                return NULL;
-            }
-        }
-#ifdef _WIN32
-        /* Windows "file:" URIs with a drive letter start with a '/' */
-        if (p[0] == '/' && p[2] == ':' && p[3] == '/') {
-            char c = tolower((unsigned char)p[1]);
-
-            if (c >= 'a' && c <= 'z') {
-                /* Skip past the slash, making the path a normal Windows path */
-                p++;
-            }
-        }
-#endif
-        path_data[path_data_n++] = p;
-    }
-
-    for (i = 0, path = NULL; path == NULL && i < path_data_n; i++) {
-        if (stat(path_data[i], &st) < 0) {
-            ERR_raise_data(ERR_LIB_SYS, errno,
-                "calling stat(%s)",
-                path_data[i]);
-        } else {
-            path = path_data[i];
-        }
-    }
-    if (path == NULL) {
-        ERR_clear_last_mark();
+    if (path == NULL)
         return NULL;
-    }
-
-    /* Successfully found a working path, clear possible collected errors */
-    ERR_pop_to_mark();

     if (S_ISDIR(st.st_mode))
         ctx = file_open_dir(path, uri, provctx);