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

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