From c7c42afab91c46e6baaa0e2801626e13acd6c30a Mon Sep 17 00:00:00 2001 From: hunterinvariants <217359725+hunterinvariants@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:14:02 +0200 Subject: [PATCH] file: support directory listing on Windows Closes #22577 --- docs/TODO.md | 7 --- lib/curlx/fopen.c | 31 ++++++++++++ lib/curlx/fopen.h | 2 + lib/file.c | 113 +++++++++++++++++++++++++++++++++++++++++--- tests/data/test3203 | 14 ++++-- 5 files changed, 150 insertions(+), 17 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 6203056994..6829910101 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -438,13 +438,6 @@ use text frames. An abandoned attempt to add support for this exists in [PR 22093](https://github.com/curl/curl/pull/22093). -# FILE - -## Directory listing on non-POSIX - -Listing the contents of a directory accessed with FILE only works on platforms -with `opendir()`. Support could be added for more systems, like Windows. - # TLS ## `TLS-PSK` with OpenSSL diff --git a/lib/curlx/fopen.c b/lib/curlx/fopen.c index 30754b310a..09264aeb10 100644 --- a/lib/curlx/fopen.c +++ b/lib/curlx/fopen.c @@ -291,6 +291,37 @@ HANDLE curlx_CreateFile(const char *filename, return handle; } + +HANDLE curlx_FindFirstFile(const char *filename, + WIN32_FIND_DATA *find_data) +{ + HANDLE handle = INVALID_HANDLE_VALUE; + +#ifdef UNICODE + TCHAR *filename_t = curlx_convert_UTF8_to_wchar(filename); +#else + const TCHAR *filename_t = filename; +#endif + + if(filename_t) { + TCHAR *fixed = NULL; + const TCHAR *target; + + if(fix_excessive_path(filename_t, &fixed)) + target = fixed; + else + target = filename_t; + + handle = FindFirstFile(target, find_data); + CURLX_FREE(fixed); + +#ifdef UNICODE + curlx_free(filename_t); +#endif + } + + return handle; +} #endif /* !CURL_WINDOWS_UWP */ int curlx_win32_open(const char *filename, int oflag, ...) diff --git a/lib/curlx/fopen.h b/lib/curlx/fopen.h index b64fbf6514..469d3c8813 100644 --- a/lib/curlx/fopen.h +++ b/lib/curlx/fopen.h @@ -43,6 +43,8 @@ HANDLE curlx_CreateFile(const char *filename, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); +HANDLE curlx_FindFirstFile(const char *filename, + WIN32_FIND_DATA *find_data); #endif /* !CURL_WINDOWS_UWP */ #define curlx_fstat _fstati64 #define curlx_struct_stat struct _stati64 diff --git a/lib/file.c b/lib/file.c index 2ebfd0c67b..d3553b7df2 100644 --- a/lib/file.c +++ b/lib/file.c @@ -76,12 +76,14 @@ struct FILEPROTO { char *freepath; /* pointer to the allocated block we must free, this might differ from the 'path' pointer */ int fd; /* open file descriptor to read from! */ + bool is_dir; }; static void file_cleanup(struct FILEPROTO *file) { curlx_safefree(file->freepath); file->path = NULL; + file->is_dir = FALSE; if(file->fd != -1) { curlx_close(file->fd); file->fd = -1; @@ -126,6 +128,38 @@ static CURLcode file_done(struct Curl_easy *data, return CURLE_OK; } +static int file_stat(const char *path, curlx_struct_stat *statbuf) +{ +#ifdef _WIN32 + int result = curlx_stat(path, statbuf); + + if(result) { + size_t pathlen = strlen(path); + + /* MSVCRT's narrow stat() rejects trailing directory separators. */ + if((pathlen > 3) && + ((path[pathlen - 1] == '\\') || (path[pathlen - 1] == '/'))) { + char *trimmed = curlx_strdup(path); + + if(trimmed) { + do { + trimmed[--pathlen] = '\0'; + } while((pathlen > 3) && + ((trimmed[pathlen - 1] == '\\') || + (trimmed[pathlen - 1] == '/'))); + + result = curlx_stat(trimmed, statbuf); + curlx_free(trimmed); + } + } + } + + return result; +#else + return curlx_stat(path, statbuf); +#endif +} + /* * file_connect() gets called from Curl_protocol_connect() to allow us to * do protocol-specific actions at connect-time. We emulate a @@ -135,6 +169,9 @@ static CURLcode file_connect(struct Curl_easy *data, bool *done) { char *real_path; struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY); +#ifdef _WIN32 + curlx_struct_stat statbuf; +#endif int fd; #ifdef DOS_FILESYSTEM size_t i; @@ -233,7 +270,13 @@ static CURLcode file_connect(struct Curl_easy *data, bool *done) file->freepath = real_path; /* free this when done */ file->fd = fd; - if(!data->state.upload && (fd == -1)) { +#ifdef _WIN32 + if(!data->state.upload && (fd == -1) && + !file_stat(file->path, &statbuf) && S_ISDIR(statbuf.st_mode)) + file->is_dir = TRUE; +#endif + + if(!data->state.upload && (fd == -1) && !file->is_dir) { failf(data, "Could not open file %s", data->state.up.path); file_done(data, CURLE_FILE_COULDNT_READ_FILE, FALSE); return CURLE_FILE_COULDNT_READ_FILE; @@ -370,6 +413,58 @@ out: return result; } +#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) +static CURLcode win32_file_list(struct Curl_easy *data, const char *path) +{ + WIN32_FIND_DATA entry; + HANDLE handle; + char *pattern; + size_t pathlen = strlen(path); + CURLcode result = CURLE_OK; + DWORD error; + + pattern = curl_maprintf("%s%s*", path, + pathlen && + (path[pathlen - 1] == '\\' || + path[pathlen - 1] == '/') ? "" : "\\"); + if(!pattern) + return CURLE_OUT_OF_MEMORY; + + handle = curlx_FindFirstFile(pattern, &entry); + curlx_free(pattern); + if(handle == INVALID_HANDLE_VALUE) + return CURLE_READ_ERROR; + + do { + if(entry.cFileName[0] != TEXT('.')) { + char *name = curlx_convert_tchar_to_UTF8(entry.cFileName); + + if(!name) { + result = CURLE_OUT_OF_MEMORY; + break; + } + + result = Curl_client_write(data, CLIENTWRITE_BODY, + name, strlen(name)); + curlx_free(name); + if(result) + break; + + result = Curl_client_write(data, CLIENTWRITE_BODY, "\n", 1); + if(result) + break; + } + } while(FindNextFile(handle, &entry)); + + error = GetLastError(); + if(!result && error != ERROR_NO_MORE_FILES) + result = CURLE_READ_ERROR; + + FindClose(handle); + return result; +} +#endif + /* * file_do() is the protocol-specific function for the do-phase, separated * from the connect-phase above. Other protocols merely setup the transfer in @@ -405,8 +500,10 @@ static CURLcode file_do(struct Curl_easy *data, bool *done) fd = file->fd; /* VMS: This only works reliable for STREAMLF files */ - if(curlx_fstat(fd, &statbuf) != -1) { - if(!S_ISDIR(statbuf.st_mode)) + if((file->is_dir ? file_stat(file->path, &statbuf) : + curlx_fstat(fd, &statbuf)) != -1) { + file->is_dir = (fd == -1) || S_ISDIR(statbuf.st_mode); + if(!file->is_dir) expected_size = statbuf.st_size; /* and store the modification time */ data->info.filetime = statbuf.st_mtime; @@ -510,7 +607,7 @@ static CURLcode file_do(struct Curl_easy *data, bool *done) Curl_pgrsSetDownloadSize(data, expected_size); if(data->state.resume_from) { - if(!S_ISDIR(statbuf.st_mode)) { + if(!file->is_dir) { if(data->state.resume_from != curl_lseek(fd, data->state.resume_from, SEEK_SET)) return CURLE_BAD_DOWNLOAD_RESUME; @@ -524,7 +621,7 @@ static CURLcode file_do(struct Curl_easy *data, bool *done) if(result) goto out; - if(!S_ISDIR(statbuf.st_mode)) { + if(!file->is_dir) { while(!result) { ssize_t nread; /* Do not fill a whole buffer if we want less than all data */ @@ -558,7 +655,11 @@ static CURLcode file_do(struct Curl_easy *data, bool *done) } } else { -#ifdef HAVE_OPENDIR +#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) + result = win32_file_list(data, file->path); + if(result) + goto out; +#elif defined(HAVE_OPENDIR) DIR *dir = opendir(file->path); struct dirent *entry; diff --git a/tests/data/test3203 b/tests/data/test3203 index 401f697419..8b52cf41cc 100644 --- a/tests/data/test3203 +++ b/tests/data/test3203 @@ -14,16 +14,18 @@ file Get a directory using file:// - - -!win32 - file://localhost%FILE_PWD/%LOGDIR/test%TESTNUMBER.dir/ +%if Unicode + +Contents of file are irrelevant + +%else Contents of file are irrelevant +%endif # Verify data after the test has been "shot" @@ -32,7 +34,11 @@ Contents of file are irrelevant 0 +%if Unicode +%hex[%E6%BC%A2%E5%AD%97%2D%C3%A4]hex%.txt +%else dir-listing-test.txt +%endif