file: support directory listing on Windows

Closes #22577
This commit is contained in:
hunterinvariants 2026-08-13 21:14:02 +02:00 committed by Daniel Stenberg
parent ebc0fbc0e8
commit c7c42afab9
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
5 changed files with 150 additions and 17 deletions

View file

@ -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

View file

@ -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, ...)

View file

@ -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

View file

@ -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;

View file

@ -14,16 +14,18 @@ file
<name>
Get a directory using file://
</name>
<!-- does not work on win32, see #6379 -->
<features>
!win32
</features>
<command option="no-include">
file://localhost%FILE_PWD/%LOGDIR/test%TESTNUMBER.dir/
</command>
%if Unicode
<file name="%LOGDIR/test%TESTNUMBER.dir/%hex[%E6%BC%A2%E5%AD%97%2D%C3%A4]hex%.txt">
Contents of file are irrelevant
</file>
%else
<file name="%LOGDIR/test%TESTNUMBER.dir/dir-listing-test.txt">
Contents of file are irrelevant
</file>
%endif
</client>
# Verify data after the test has been "shot"
@ -32,7 +34,11 @@ Contents of file are irrelevant
0
</errorcode>
<stdout>
%if Unicode
%hex[%E6%BC%A2%E5%AD%97%2D%C3%A4]hex%.txt
%else
dir-listing-test.txt
%endif
</stdout>
</verify>
</testcase>