Coverage Report

Created: 2026-10-15 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl/lib/curlx/win32-fopen.c
Line
Count
Source
1
/***************************************************************************
2
 *                                  _   _ ____  _
3
 *  Project                     ___| | | |  _ \| |
4
 *                             / __| | | | |_) | |
5
 *                            | (__| |_| |  _ <| |___
6
 *                             \___|\___/|_| \_\_____|
7
 *
8
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
9
 *
10
 * This software is licensed as described in the file COPYING, which
11
 * you should have received as part of this distribution. The terms
12
 * are also available at https://curl.se/docs/copyright.html.
13
 *
14
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15
 * copies of the Software, and permit persons to whom the Software is
16
 * furnished to do so, under the terms of the COPYING file.
17
 *
18
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19
 * KIND, either express or implied.
20
 *
21
 * SPDX-License-Identifier: curl
22
 *
23
 ***************************************************************************/
24
#include "curl_setup.h"
25
26
#include "curlx/win32-fopen.h"
27
28
int curlx_fseek(void *stream, curl_off_t offset, int whence)
29
0
{
30
#ifdef _WIN32
31
  return _fseeki64(stream, (__int64)offset, whence);
32
#elif defined(HAVE_FSEEKO) && defined(HAVE_DECL_FSEEKO)
33
  return fseeko(stream, (off_t)offset, whence);
34
#else
35
  if(offset > LONG_MAX)
36
    return -1;
37
  return fseek(stream, (long)offset, whence);
38
#endif
39
0
}
40
41
#ifdef _WIN32
42
43
#include <direct.h>  /* for _mkdir(), _wmkdir() */
44
#include <share.h>  /* for _SH_DENYNO */
45
46
#include "curlx/multibyte.h"
47
#include "curlx/timeval.h"
48
49
#ifdef CURL_MEMDEBUG
50
/*
51
 * Use system allocators to avoid infinite recursion when called by curl's
52
 * memory tracker memdebug functions.
53
 */
54
#define CURLX_MALLOC(x) malloc(x)
55
#define CURLX_FREE(x)   free(x)
56
#else
57
#define CURLX_MALLOC(x) curlx_malloc(x)
58
#define CURLX_FREE(x)   curlx_free(x)
59
#endif
60
61
#ifdef _UNICODE
62
static wchar_t *fn_convert_UTF8_to_wchar(const char *str_utf8)
63
{
64
  wchar_t *str_w = NULL;
65
66
  if(str_utf8) {
67
    int str_w_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
68
                                        str_utf8, -1, NULL, 0);
69
    if(str_w_len > 0) {
70
      str_w = CURLX_MALLOC(str_w_len * sizeof(wchar_t));
71
      if(str_w) {
72
        if(MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
73
                               str_utf8, -1, str_w, str_w_len) == 0) {
74
          CURLX_FREE(str_w);
75
          return NULL;
76
        }
77
      }
78
    }
79
  }
80
  return str_w;
81
}
82
#endif
83
84
/* declare GetFullPathNameW for mingw-w64 UWP builds targeting old Windows */
85
#if defined(CURL_WINDOWS_UWP) && defined(__MINGW32__) && \
86
  (_WIN32_WINNT < _WIN32_WINNT_WIN10)
87
WINBASEAPI DWORD WINAPI GetFullPathNameW(LPCWSTR, DWORD, LPWSTR, LPWSTR *);
88
#endif
89
90
/* Fix excessive paths (paths that exceed MAX_PATH length of 260).
91
 *
92
 * This is a helper function to fix paths that would exceed the MAX_PATH
93
 * limitation check done by Windows APIs. It does so by normalizing the passed
94
 * in filename or path 'in' to its full canonical path, and if that path is
95
 * longer than MAX_PATH then setting 'out' to "\\?\" prefix + that full path.
96
 *
97
 * For example 'in' filename255chars in current directory C:\foo\bar is
98
 * fixed as \\?\C:\foo\bar\filename255chars for 'out' which tells Windows
99
 * it is ok to access that filename even though the actual full path is longer
100
 * than 260 chars.
101
 *
102
 * For non-Unicode builds this function may fail sometimes because only the
103
 * Unicode versions of some Windows API functions can access paths longer than
104
 * MAX_PATH, for example GetFullPathNameW which is used in this function. When
105
 * the full path is then converted from Unicode to multibyte that fails if any
106
 * directories in the path contain characters not in the current codepage.
107
 */
108
static bool fix_excessive_path(const TCHAR *in, TCHAR **out)
109
{
110
  size_t needed, count;
111
  const wchar_t *in_w;
112
  wchar_t *fbuf = NULL;
113
114
  /* MS-documented "approximate" limit for the maximum path length */
115
  const size_t max_path_len = 32767;
116
117
#ifndef _UNICODE
118
  wchar_t *ibuf = NULL;
119
  char *obuf = NULL;
120
#endif
121
122
  *out = NULL;
123
124
  /* skip paths already normalized */
125
  if(!_tcsncmp(in, _TEXT("\\\\?\\"), 4))
126
    goto cleanup;
127
128
#ifndef _UNICODE
129
  /* convert multibyte input to unicode */
130
  if(mbstowcs_s(&needed, NULL, 0, in, 0))
131
    goto cleanup;
132
  if(!needed || needed >= max_path_len)
133
    goto cleanup;
134
  ibuf = CURLX_MALLOC(needed * sizeof(wchar_t));
135
  if(!ibuf)
136
    goto cleanup;
137
  if(mbstowcs_s(&count, ibuf, needed, in, needed - 1))
138
    goto cleanup;
139
  if(count != needed)
140
    goto cleanup;
141
  in_w = ibuf;
142
#else
143
  in_w = in;
144
#endif
145
146
  /* GetFullPathNameW returns the normalized full path in unicode. It converts
147
     forward slashes to backslashes, processes .. to remove directory segments,
148
     etc. Unlike GetFullPathNameA it can process paths that exceed MAX_PATH. */
149
  needed = (size_t)GetFullPathNameW(in_w, 0, NULL, NULL);
150
  if(!needed || needed > max_path_len)
151
    goto cleanup;
152
  /* skip paths that are not excessive and do not need modification */
153
  if(needed <= MAX_PATH)
154
    goto cleanup;
155
  fbuf = CURLX_MALLOC(needed * sizeof(wchar_t));
156
  if(!fbuf)
157
    goto cleanup;
158
  count = (size_t)GetFullPathNameW(in_w, (DWORD)needed, fbuf, NULL);
159
  if(!count || count >= needed)
160
    goto cleanup;
161
162
  /* prepend \\?\ or \\?\UNC\ to the excessively long path.
163
   *
164
   * c:\longpath            --->    \\?\c:\longpath
165
   * \\.\c:\longpath        --->    \\?\c:\longpath
166
   * \\?\c:\longpath        --->    \\?\c:\longpath  (unchanged)
167
   * \\server\c$\longpath   --->    \\?\UNC\server\c$\longpath
168
   *
169
   * https://learn.microsoft.com/dotnet/standard/io/file-path-formats
170
   */
171
  if(!wcsncmp(fbuf, L"\\\\?\\", 4))
172
    ; /* do nothing */
173
  else if(!wcsncmp(fbuf, L"\\\\.\\", 4))
174
    fbuf[2] = '?';
175
  else if(!wcsncmp(fbuf, L"\\\\.", 3) || !wcsncmp(fbuf, L"\\\\?", 3)) {
176
    /* Unexpected, not UNC. The formatting doc does not allow this AFAICT. */
177
    goto cleanup;
178
  }
179
  else {
180
    wchar_t *temp;
181
182
    if(!wcsncmp(fbuf, L"\\\\", 2)) {
183
      /* "\\?\UNC\" + full path without "\\" + null */
184
      needed = 8 + (count - 2) + 1;
185
      if(needed > max_path_len)
186
        goto cleanup;
187
188
      temp = CURLX_MALLOC(needed * sizeof(wchar_t));
189
      if(!temp)
190
        goto cleanup;
191
192
      if(wcsncpy_s(temp, needed, L"\\\\?\\UNC\\", 8)) {
193
        CURLX_FREE(temp);
194
        goto cleanup;
195
      }
196
      if(wcscpy_s(temp + 8, needed, fbuf + 2)) {
197
        CURLX_FREE(temp);
198
        goto cleanup;
199
      }
200
    }
201
    else {
202
      /* "\\?\" + full path + null */
203
      needed = 4 + count + 1;
204
      if(needed > max_path_len)
205
        goto cleanup;
206
207
      temp = CURLX_MALLOC(needed * sizeof(wchar_t));
208
      if(!temp)
209
        goto cleanup;
210
211
      if(wcsncpy_s(temp, needed, L"\\\\?\\", 4)) {
212
        CURLX_FREE(temp);
213
        goto cleanup;
214
      }
215
      if(wcscpy_s(temp + 4, needed, fbuf)) {
216
        CURLX_FREE(temp);
217
        goto cleanup;
218
      }
219
    }
220
221
    CURLX_FREE(fbuf);
222
    fbuf = temp;
223
  }
224
225
#ifndef _UNICODE
226
  /* convert unicode full path to multibyte output */
227
  if(wcstombs_s(&needed, NULL, 0, fbuf, 0))
228
    goto cleanup;
229
  if(!needed || needed >= max_path_len)
230
    goto cleanup;
231
  obuf = CURLX_MALLOC(needed);
232
  if(!obuf)
233
    goto cleanup;
234
  if(wcstombs_s(&count, obuf, needed, fbuf, needed - 1))
235
    goto cleanup;
236
  if(count != needed)
237
    goto cleanup;
238
  *out = obuf;
239
  obuf = NULL;
240
#else
241
  *out = fbuf;
242
  fbuf = NULL;
243
#endif
244
245
cleanup:
246
  CURLX_FREE(fbuf);
247
#ifndef _UNICODE
248
  CURLX_FREE(ibuf);
249
  CURLX_FREE(obuf);
250
#endif
251
  return !!*out;
252
}
253
254
#ifndef CURL_WINDOWS_UWP
255
HANDLE curlx_CreateFile(const char *filename,
256
                        DWORD dwDesiredAccess,
257
                        DWORD dwShareMode,
258
                        LPSECURITY_ATTRIBUTES lpSecurityAttributes,
259
                        DWORD dwCreationDisposition,
260
                        DWORD dwFlagsAndAttributes,
261
                        HANDLE hTemplateFile)
262
{
263
  HANDLE handle = INVALID_HANDLE_VALUE;
264
265
#ifdef UNICODE
266
  TCHAR *filename_t = curlx_convert_UTF8_to_wchar(filename);
267
#else
268
  const TCHAR *filename_t = filename;
269
#endif
270
271
  if(filename_t) {
272
    TCHAR *fixed = NULL;
273
    const TCHAR *target = NULL;
274
275
    if(fix_excessive_path(filename_t, &fixed))
276
      target = fixed;
277
    else
278
      target = filename_t;
279
    /* !checksrc! disable BANNEDFUNC 1 */
280
    handle = CreateFile(target,
281
                        dwDesiredAccess,
282
                        dwShareMode,
283
                        lpSecurityAttributes,
284
                        dwCreationDisposition,
285
                        dwFlagsAndAttributes,
286
                        hTemplateFile);
287
    CURLX_FREE(fixed);
288
#ifdef UNICODE
289
    curlx_free(filename_t);
290
#endif
291
  }
292
293
  return handle;
294
}
295
296
HANDLE curlx_FindFirstFile(const char *filename,
297
                           WIN32_FIND_DATA *find_data)
298
{
299
  HANDLE handle = INVALID_HANDLE_VALUE;
300
301
#ifdef UNICODE
302
  TCHAR *filename_t = curlx_convert_UTF8_to_wchar(filename);
303
#else
304
  const TCHAR *filename_t = filename;
305
#endif
306
307
  if(filename_t) {
308
    TCHAR *fixed = NULL;
309
    const TCHAR *target;
310
311
    if(fix_excessive_path(filename_t, &fixed))
312
      target = fixed;
313
    else
314
      target = filename_t;
315
316
    handle = FindFirstFile(target, find_data);
317
    CURLX_FREE(fixed);
318
319
#ifdef UNICODE
320
    curlx_free(filename_t);
321
#endif
322
  }
323
324
  return handle;
325
}
326
#endif /* !CURL_WINDOWS_UWP */
327
328
int curlx_win32_open(const char *filename, int oflag, ...)
329
{
330
  int pmode = 0;
331
  int res = -1;
332
  TCHAR *fixed = NULL;
333
  const TCHAR *target = NULL;
334
335
#ifdef _UNICODE
336
  wchar_t *filename_w = fn_convert_UTF8_to_wchar(filename);
337
#endif
338
339
  va_list param;
340
  va_start(param, oflag);
341
  if(oflag & O_CREAT)
342
    pmode = va_arg(param, int);
343
  va_end(param);
344
345
#ifdef _UNICODE
346
  if(filename_w) {
347
    if(fix_excessive_path(filename_w, &fixed))
348
      target = fixed;
349
    else
350
      target = filename_w;
351
    errno = _wsopen_s(&res, target, oflag, _SH_DENYNO, pmode);
352
    CURLX_FREE(filename_w);
353
  }
354
  else
355
    /* !checksrc! disable ERRNOVAR 1 */
356
    errno = EINVAL;
357
#else
358
  if(fix_excessive_path(filename, &fixed))
359
    target = fixed;
360
  else
361
    target = filename;
362
  errno = _sopen_s(&res, target, oflag, _SH_DENYNO, pmode);
363
#endif
364
365
  CURLX_FREE(fixed);
366
  return res;
367
}
368
369
FILE *curlx_win32_fopen(const char *filename, const char *mode)
370
{
371
  FILE *file = NULL;
372
  TCHAR *fixed = NULL;
373
  const TCHAR *target = NULL;
374
375
#ifdef _UNICODE
376
  wchar_t *filename_w = fn_convert_UTF8_to_wchar(filename);
377
  wchar_t *mode_w = fn_convert_UTF8_to_wchar(mode);
378
  if(filename_w && mode_w) {
379
    if(fix_excessive_path(filename_w, &fixed))
380
      target = fixed;
381
    else
382
      target = filename_w;
383
    file = _wfsopen(target, mode_w, _SH_DENYNO);
384
  }
385
  else
386
    /* !checksrc! disable ERRNOVAR 1 */
387
    errno = EINVAL;
388
  CURLX_FREE(filename_w);
389
  CURLX_FREE(mode_w);
390
#else
391
  if(fix_excessive_path(filename, &fixed))
392
    target = fixed;
393
  else
394
    target = filename;
395
  file = _fsopen(target, mode, _SH_DENYNO);
396
#endif
397
398
  CURLX_FREE(fixed);
399
  return file;
400
}
401
402
#if defined(__MINGW32__) && (__MINGW64_VERSION_MAJOR < 5)
403
_CRTIMP errno_t __cdecl freopen_s(FILE **file, const char *filename,
404
                                  const char *mode, FILE *stream);
405
#endif
406
407
FILE *curlx_win32_freopen(const char *filename, const char *mode, FILE *fp)
408
{
409
  FILE *file = NULL;
410
  TCHAR *fixed = NULL;
411
  const TCHAR *target = NULL;
412
413
#ifdef _UNICODE
414
  wchar_t *filename_w = fn_convert_UTF8_to_wchar(filename);
415
  wchar_t *mode_w = fn_convert_UTF8_to_wchar(mode);
416
  if(filename_w && mode_w) {
417
    if(fix_excessive_path(filename_w, &fixed))
418
      target = fixed;
419
    else
420
      target = filename_w;
421
    errno = _wfreopen_s(&file, target, mode_w, fp);
422
  }
423
  else
424
    /* !checksrc! disable ERRNOVAR 1 */
425
    errno = EINVAL;
426
  CURLX_FREE(filename_w);
427
  CURLX_FREE(mode_w);
428
#else
429
  if(fix_excessive_path(filename, &fixed))
430
    target = fixed;
431
  else
432
    target = filename;
433
  errno = freopen_s(&file, target, mode, fp);
434
#endif
435
436
  CURLX_FREE(fixed);
437
  return file;
438
}
439
440
int curlx_win32_stat(const char *path, curlx_struct_stat *buffer)
441
{
442
  int res = -1;
443
  TCHAR *fixed = NULL;
444
  const TCHAR *target = NULL;
445
446
#ifdef _UNICODE
447
  wchar_t *path_w = curlx_convert_UTF8_to_wchar(path);
448
  if(path_w) {
449
    if(fix_excessive_path(path_w, &fixed))
450
      target = fixed;
451
    else
452
      target = path_w;
453
    res = _wstati64(target, buffer);
454
    curlx_free(path_w);
455
  }
456
  else
457
    /* !checksrc! disable ERRNOVAR 1 */
458
    errno = EINVAL;
459
#else
460
  if(fix_excessive_path(path, &fixed))
461
    target = fixed;
462
  else
463
    target = path;
464
  res = _stati64(target, buffer);
465
#endif
466
467
  CURLX_FREE(fixed);
468
  return res;
469
}
470
471
int curlx_win32_mkdir(const char *path)
472
{
473
  int res = -1;
474
  TCHAR *fixed = NULL;
475
  const TCHAR *target = NULL;
476
477
#ifdef _UNICODE
478
  wchar_t *path_w = curlx_convert_UTF8_to_wchar(path);
479
  if(path_w) {
480
    if(fix_excessive_path(path_w, &fixed))
481
      target = fixed;
482
    else
483
      target = path_w;
484
    res = _wmkdir(target);
485
    curlx_free(path_w);
486
  }
487
  else
488
    /* !checksrc! disable ERRNOVAR 1 */
489
    errno = EINVAL;
490
#else
491
  if(fix_excessive_path(path, &fixed))
492
    target = fixed;
493
  else
494
    target = path;
495
  res = _mkdir(target);
496
#endif
497
498
  CURLX_FREE(fixed);
499
  return res;
500
}
501
502
#if !defined(CURL_DISABLE_HTTP) || !defined(CURL_DISABLE_COOKIES) || \
503
  !defined(CURL_DISABLE_ALTSVC)
504
/* rename() on Windows does not overwrite, so we cannot use it here.
505
   MoveFileEx() does overwrite and is usually atomic but fails when there are
506
   open handles to the file. */
507
int curlx_win32_rename(const char *oldpath, const char *newpath)
508
{
509
  int res = -1; /* fail */
510
511
#ifdef UNICODE
512
  TCHAR *tchar_oldpath = curlx_convert_UTF8_to_wchar(oldpath);
513
  TCHAR *tchar_newpath = curlx_convert_UTF8_to_wchar(newpath);
514
#else
515
  const TCHAR *tchar_oldpath = oldpath;
516
  const TCHAR *tchar_newpath = newpath;
517
#endif
518
519
  if(tchar_oldpath && tchar_newpath) {
520
    const int max_wait_ms = 1000;
521
    struct curltime start;
522
523
    TCHAR *oldpath_fixed = NULL;
524
    TCHAR *newpath_fixed = NULL;
525
    const TCHAR *target_oldpath;
526
    const TCHAR *target_newpath;
527
528
    if(fix_excessive_path(tchar_oldpath, &oldpath_fixed))
529
      target_oldpath = oldpath_fixed;
530
    else
531
      target_oldpath = tchar_oldpath;
532
533
    if(fix_excessive_path(tchar_newpath, &newpath_fixed))
534
      target_newpath = newpath_fixed;
535
    else
536
      target_newpath = tchar_newpath;
537
538
    start = curlx_now();
539
540
    for(;;) {
541
      timediff_t diff;
542
      /* !checksrc! disable BANNEDFUNC 1 */
543
      if(MoveFileEx(target_oldpath, target_newpath,
544
                    MOVEFILE_REPLACE_EXISTING)) {
545
        res = 0; /* success */
546
        break;
547
      }
548
      diff = curlx_timediff_ms(curlx_now(), start);
549
      if(diff < 0 || diff > max_wait_ms) {
550
        break;
551
      }
552
      Sleep(1);
553
    }
554
555
    CURLX_FREE(oldpath_fixed);
556
    CURLX_FREE(newpath_fixed);
557
  }
558
559
#ifdef UNICODE
560
  curlx_free(tchar_oldpath);
561
  curlx_free(tchar_newpath);
562
#endif
563
564
  return res;
565
}
566
#endif
567
568
#undef CURLX_MALLOC
569
#undef CURLX_FREE
570
571
#endif /* _WIN32 */