Coverage Report

Created: 2026-08-13 07:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl/lib/file.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
#include "urldata.h"
26
#include "file.h"
27
28
#ifndef CURL_DISABLE_FILE
29
30
#ifdef HAVE_NETINET_IN_H
31
#include <netinet/in.h>
32
#endif
33
#ifdef HAVE_NETDB_H
34
#include <netdb.h>
35
#endif
36
#ifdef HAVE_ARPA_INET_H
37
#include <arpa/inet.h>
38
#endif
39
#ifdef HAVE_NET_IF_H
40
#include <net/if.h>
41
#endif
42
#ifdef HAVE_SYS_IOCTL_H
43
#include <sys/ioctl.h>
44
#endif
45
46
#ifdef HAVE_SYS_PARAM_H
47
#include <sys/param.h>
48
#endif
49
50
#ifdef HAVE_DIRENT_H
51
#include <dirent.h>
52
#endif
53
54
#include "progress.h"
55
#include "sendf.h"
56
#include "curl_trc.h"
57
#include "escape.h"
58
#include "multiif.h"
59
#include "transfer.h"
60
#include "url.h"
61
#include "parsedate.h" /* for the week day and month names */
62
#include "curlx/fopen.h"
63
#include "curl_range.h"
64
65
#if defined(_WIN32) || defined(MSDOS)
66
#define DOS_FILESYSTEM 1
67
#elif defined(__amigaos4__)
68
#define AMIGA_FILESYSTEM 1
69
#endif
70
71
/* meta key for storing protocol meta at easy handle */
72
7.93k
#define CURL_META_FILE_EASY   "meta:proto:file:easy"
73
74
struct FILEPROTO {
75
  char *path; /* the path we operate on */
76
  char *freepath; /* pointer to the allocated block we must free, this might
77
                     differ from the 'path' pointer */
78
  int fd;     /* open file descriptor to read from! */
79
};
80
81
static void file_cleanup(struct FILEPROTO *file)
82
2.85k
{
83
2.85k
  curlx_safefree(file->freepath);
84
2.85k
  file->path = NULL;
85
2.85k
  if(file->fd != -1) {
86
1.19k
    curlx_close(file->fd);
87
1.19k
    file->fd = -1;
88
1.19k
  }
89
2.85k
}
90
91
static void file_easy_dtor(void *key, size_t klen, void *entry)
92
1.36k
{
93
1.36k
  struct FILEPROTO *file = entry;
94
1.36k
  (void)key;
95
1.36k
  (void)klen;
96
1.36k
  file_cleanup(file);
97
1.36k
  curlx_free(file);
98
1.36k
}
99
100
static CURLcode file_setup_connection(struct Curl_easy *data,
101
                                      struct connectdata *conn)
102
1.36k
{
103
1.36k
  struct FILEPROTO *filep;
104
1.36k
  (void)conn;
105
  /* allocate the FILE specific struct */
106
1.36k
  filep = curlx_calloc(1, sizeof(*filep));
107
1.36k
  if(filep)
108
1.36k
    filep->fd = -1;
109
1.36k
  if(!filep ||
110
1.36k
     Curl_meta_set(data, CURL_META_FILE_EASY, filep, file_easy_dtor))
111
0
    return CURLE_OUT_OF_MEMORY;
112
113
1.36k
  return CURLE_OK;
114
1.36k
}
115
116
static CURLcode file_done(struct Curl_easy *data,
117
                          CURLcode status, bool premature)
118
2.73k
{
119
2.73k
  struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
120
2.73k
  (void)status;
121
2.73k
  (void)premature;
122
123
2.73k
  if(file)
124
1.48k
    file_cleanup(file);
125
126
2.73k
  return CURLE_OK;
127
2.73k
}
128
129
/*
130
 * file_connect() gets called from Curl_protocol_connect() to allow us to
131
 * do protocol-specific actions at connect-time. We emulate a
132
 * connect-then-transfer protocol and "connect" to the file here
133
 */
134
static CURLcode file_connect(struct Curl_easy *data, bool *done)
135
2.60k
{
136
2.60k
  char *real_path;
137
2.60k
  struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
138
2.60k
  int fd;
139
#ifdef DOS_FILESYSTEM
140
  size_t i;
141
  char *actual_path;
142
#endif
143
2.60k
  size_t real_path_len;
144
2.60k
  CURLcode result;
145
146
2.60k
  if(!file)
147
0
    return CURLE_FAILED_INIT;
148
149
2.60k
  if(file->path) {
150
    /* already connected.
151
     * the handler->connect_it() is normally only called once, but
152
     * FILE does a special check on setting up the connection which
153
     * calls this explicitly. */
154
1.23k
    *done = TRUE;
155
1.23k
    return CURLE_OK;
156
1.23k
  }
157
158
1.36k
  result = Curl_urldecode(data->state.up.path, 0, &real_path,
159
1.36k
                          &real_path_len, REJECT_ZERO);
160
1.36k
  if(result)
161
1
    return result;
162
163
#ifdef DOS_FILESYSTEM
164
  /* If the first character is a slash, and there is
165
     something that looks like a drive at the beginning of
166
     the path, skip the slash. If we remove the initial
167
     slash in all cases, paths without drive letters end up
168
     relative to the current directory which is not how
169
     browsers work.
170
171
     Some browsers accept | instead of : as the drive letter
172
     separator, so we do too.
173
174
     On other platforms, we need the slash to indicate an
175
     absolute pathname. On Windows, absolute paths start
176
     with a drive letter. */
177
  actual_path = real_path;
178
  if((actual_path[0] == '/') &&
179
      actual_path[1] &&
180
     (actual_path[2] == ':' || actual_path[2] == '|')) {
181
    actual_path[2] = ':';
182
    actual_path++;
183
    real_path_len--;
184
  }
185
186
  /* change path separators from '/' to '\\' for DOS, Windows and OS/2 */
187
  for(i = 0; i < real_path_len; ++i)
188
    if(actual_path[i] == '/')
189
      actual_path[i] = '\\';
190
    else if(!actual_path[i]) { /* binary zero */
191
      curlx_safefree(real_path);
192
      return CURLE_URL_MALFORMAT;
193
    }
194
195
  fd = curlx_open(actual_path, O_RDONLY | CURL_O_BINARY);
196
  file->path = actual_path;
197
#else
198
1.36k
  if(memchr(real_path, 0, real_path_len)) {
199
    /* binary zeroes indicate foul play */
200
0
    curlx_safefree(real_path);
201
0
    return CURLE_URL_MALFORMAT;
202
0
  }
203
204
#ifdef AMIGA_FILESYSTEM
205
  /*
206
   * A leading slash in an AmigaDOS path denotes the parent
207
   * directory, and hence we block this as it is relative.
208
   * Absolute paths start with 'volumename:', so we check for
209
   * this first. Failing that, we treat the path as a real Unix
210
   * path, but only if the application was compiled with -lunix.
211
   */
212
  fd = -1;
213
  file->path = real_path;
214
215
  if(real_path[0] == '/') {
216
    extern int __unix_path_semantics;
217
    if(strchr(real_path + 1, ':')) {
218
      /* Amiga absolute path */
219
      fd = curlx_open(real_path + 1, O_RDONLY);
220
      file->path++;
221
    }
222
    else if(__unix_path_semantics) {
223
      /* -lunix fallback */
224
      fd = curlx_open(real_path, O_RDONLY);
225
    }
226
  }
227
#else
228
1.36k
  fd = curlx_open(real_path, O_RDONLY);
229
1.36k
  file->path = real_path;
230
1.36k
#endif
231
1.36k
#endif
232
1.36k
  curlx_free(file->freepath);
233
1.36k
  file->freepath = real_path; /* free this when done */
234
235
1.36k
  file->fd = fd;
236
1.36k
  if(!data->state.upload && (fd == -1)) {
237
120
    failf(data, "Could not open file %s", data->state.up.path);
238
120
    file_done(data, CURLE_FILE_COULDNT_READ_FILE, FALSE);
239
120
    return CURLE_FILE_COULDNT_READ_FILE;
240
120
  }
241
1.24k
  *done = TRUE;
242
243
1.24k
  return CURLE_OK;
244
1.36k
}
245
246
static CURLcode file_disconnect(struct Curl_easy *data,
247
                                struct connectdata *conn,
248
                                bool dead_connection)
249
1.36k
{
250
1.36k
  (void)dead_connection;
251
1.36k
  (void)conn;
252
1.36k
  return file_done(data, CURLE_OK, FALSE);
253
1.36k
}
254
255
#ifdef DOS_FILESYSTEM
256
#define DIRSEP '\\'
257
#else
258
63
#define DIRSEP '/'
259
#endif
260
261
static CURLcode file_upload(struct Curl_easy *data,
262
                            struct FILEPROTO *file)
263
63
{
264
63
  const char *dir = strchr(file->path, DIRSEP);
265
63
  int fd;
266
63
  int mode;
267
63
  CURLcode result = CURLE_OK;
268
63
  char *xfer_ulbuf;
269
63
  size_t xfer_ulblen;
270
63
  curlx_struct_stat file_stat;
271
63
  const char *sendbuf;
272
63
  bool eos = FALSE;
273
274
  /*
275
   * Since FILE: does not do the full init, we need to provide some extra
276
   * assignments here.
277
   */
278
279
63
  if(!dir)
280
0
    return CURLE_FILE_COULDNT_READ_FILE; /* fix: better error code */
281
282
63
  if(!dir[1])
283
3
    return CURLE_FILE_COULDNT_READ_FILE; /* fix: better error code */
284
285
60
  mode = O_WRONLY | O_CREAT | CURL_O_BINARY;
286
60
  if(data->state.resume_from)
287
27
    mode |= O_APPEND;
288
33
  else
289
33
    mode |= O_TRUNC;
290
291
#ifdef _WIN32
292
  fd = curlx_open(file->path, mode,
293
                  data->set.new_file_perms & (_S_IREAD | _S_IWRITE));
294
#elif (defined(ANDROID) || defined(__ANDROID__)) && \
295
  (defined(__i386__) || defined(__arm__))
296
  fd = curlx_open(file->path, mode, (mode_t)data->set.new_file_perms);
297
#else
298
60
  fd = curlx_open(file->path, mode, data->set.new_file_perms);
299
60
#endif
300
60
  if(fd < 0) {
301
28
    failf(data, "cannot open %s for writing", file->path);
302
28
    return CURLE_WRITE_ERROR;
303
28
  }
304
305
32
  if(data->state.infilesize != -1)
306
    /* known size of data to "upload" */
307
32
    Curl_pgrsSetUploadSize(data, data->state.infilesize);
308
309
  /* treat the negative resume offset value as the case of "-" */
310
32
  if(data->state.resume_from < 0) {
311
0
    if(curlx_fstat(fd, &file_stat)) {
312
0
      curlx_close(fd);
313
0
      failf(data, "cannot get the size of %s", file->path);
314
0
      return CURLE_WRITE_ERROR;
315
0
    }
316
0
    data->state.resume_from = (curl_off_t)file_stat.st_size;
317
0
  }
318
319
32
  result = Curl_multi_xfer_ulbuf_borrow(data, &xfer_ulbuf, &xfer_ulblen);
320
32
  if(result)
321
0
    goto out;
322
323
33
  while(!result && !eos) {
324
32
    size_t nread, nwritten;
325
32
    ssize_t rv;
326
32
    size_t readcount;
327
328
32
    result = Curl_client_read(data, xfer_ulbuf, xfer_ulblen, &readcount, &eos);
329
32
    if(result)
330
0
      break;
331
332
32
    if(!readcount)
333
31
      break;
334
335
1
    nread = readcount;
336
337
    /* skip bytes before resume point */
338
1
    if(data->state.resume_from) {
339
0
      if((curl_off_t)nread <= data->state.resume_from) {
340
0
        data->state.resume_from -= nread;
341
0
        nread = 0;
342
0
        sendbuf = xfer_ulbuf;
343
0
      }
344
0
      else {
345
0
        sendbuf = xfer_ulbuf + data->state.resume_from;
346
0
        nread -= (size_t)data->state.resume_from;
347
0
        data->state.resume_from = 0;
348
0
      }
349
0
    }
350
1
    else
351
1
      sendbuf = xfer_ulbuf;
352
353
    /* write the data to the target */
354
1
    rv = write(fd, sendbuf, nread);
355
1
    if(!curlx_sztouz(rv, &nwritten) || (nwritten != nread)) {
356
0
      result = CURLE_SEND_ERROR;
357
0
      break;
358
0
    }
359
1
    Curl_pgrs_upload_inc(data, nwritten);
360
361
1
    result = Curl_pgrsCheck(data);
362
1
  }
363
32
  if(!result)
364
32
    result = Curl_pgrsUpdate(data);
365
366
32
out:
367
32
  curlx_close(fd);
368
32
  Curl_multi_xfer_ulbuf_release(data, xfer_ulbuf);
369
370
32
  return result;
371
32
}
372
373
/*
374
 * file_do() is the protocol-specific function for the do-phase, separated
375
 * from the connect-phase above. Other protocols merely setup the transfer in
376
 * the do-phase, to have it done in the main transfer loop but since some
377
 * platforms we support do not allow select()ing etc on file handles (as
378
 * opposed to sockets) we instead perform the whole do-operation in this
379
 * function.
380
 */
381
static CURLcode file_do(struct Curl_easy *data, bool *done)
382
1.23k
{
383
  /* This implementation ignores the hostname in conformance with
384
     RFC 1738. Only local files (reachable via the standard file system)
385
     are supported. This means that files on remotely mounted directories
386
     (via NFS, Samba, NT sharing) can be accessed through a file:// URL */
387
1.23k
  struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
388
1.23k
  CURLcode result = CURLE_OK;
389
1.23k
  curlx_struct_stat statbuf;
390
1.23k
  curl_off_t expected_size = -1;
391
1.23k
  bool size_known;
392
1.23k
  bool fstated = FALSE;
393
1.23k
  int fd;
394
1.23k
  char *xfer_buf;
395
1.23k
  size_t xfer_blen;
396
397
1.23k
  *done = TRUE; /* unconditionally */
398
1.23k
  if(!file)
399
0
    return CURLE_FAILED_INIT;
400
401
1.23k
  if(data->state.upload)
402
63
    return file_upload(data, file);
403
404
  /* get the fd from the connection phase */
405
1.17k
  fd = file->fd;
406
407
  /* VMS: This only works reliable for STREAMLF files */
408
1.17k
  if(curlx_fstat(fd, &statbuf) != -1) {
409
1.17k
    if(!S_ISDIR(statbuf.st_mode))
410
378
      expected_size = statbuf.st_size;
411
    /* and store the modification time */
412
1.17k
    data->info.filetime = statbuf.st_mtime;
413
1.17k
    fstated = TRUE;
414
1.17k
  }
415
416
1.17k
  if(fstated && !data->state.range && data->set.timecondition &&
417
86
     !Curl_meets_timecondition(data, data->info.filetime))
418
38
    return CURLE_OK;
419
420
1.13k
  if(fstated) {
421
1.13k
    time_t filetime;
422
1.13k
    struct tm buffer;
423
1.13k
    const struct tm *tm = &buffer;
424
1.13k
    char header[80];
425
1.13k
    int headerlen;
426
1.13k
    static const char accept_ranges[] = { "Accept-ranges: bytes\r\n" };
427
1.13k
    if(expected_size >= 0) {
428
378
      headerlen =
429
378
        curl_msnprintf(header, sizeof(header),
430
378
                       "Content-Length: %" FMT_OFF_T "\r\n", expected_size);
431
378
      result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
432
378
      if(result)
433
1
        return result;
434
435
377
      result = Curl_client_write(data, CLIENTWRITE_HEADER,
436
377
                                 accept_ranges, CURL_CSTRLEN(accept_ranges));
437
377
      if(result != CURLE_OK)
438
1
        return result;
439
377
    }
440
441
1.13k
    filetime = (time_t)statbuf.st_mtime;
442
1.13k
    result = curlx_gmtime(filetime, &buffer);
443
1.13k
    if(result)
444
0
      return result;
445
446
    /* format: "Tue, 15 Nov 1994 12:45:26 GMT" */
447
1.13k
    headerlen =
448
1.13k
      curl_msnprintf(header, sizeof(header),
449
1.13k
                     "Last-Modified: %s, %02d %s %4d %02d:%02d:%02d GMT\r\n",
450
1.13k
                     Curl_wkday[tm->tm_wday ? tm->tm_wday - 1 : 6],
451
1.13k
                     tm->tm_mday,
452
1.13k
                     Curl_month[tm->tm_mon],
453
1.13k
                     tm->tm_year + 1900,
454
1.13k
                     tm->tm_hour,
455
1.13k
                     tm->tm_min,
456
1.13k
                     tm->tm_sec);
457
1.13k
    result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
458
1.13k
    if(!result)
459
      /* end of headers */
460
1.12k
      result = Curl_client_write(data, CLIENTWRITE_HEADER, "\r\n", 2);
461
1.13k
    if(result)
462
6
      return result;
463
    /* set the file size to make it available post transfer */
464
1.12k
    Curl_pgrsSetDownloadSize(data, expected_size);
465
1.12k
    if(data->req.no_body)
466
1
      return CURLE_OK;
467
1.12k
  }
468
469
  /* Check whether file range has been specified */
470
1.12k
  result = Curl_range(data);
471
1.12k
  if(result)
472
68
    return result;
473
474
  /* Adjust the start offset in case we want to get the N last bytes
475
   * of the stream if the filesize could be determined */
476
1.05k
  if(data->state.resume_from < 0) {
477
200
    if(!fstated) {
478
0
      failf(data, "cannot get the size of file.");
479
0
      return CURLE_READ_ERROR;
480
0
    }
481
200
    data->state.resume_from += (curl_off_t)statbuf.st_size;
482
200
  }
483
484
1.05k
  if(data->state.resume_from > 0) {
485
    /* We check explicitly if we have a start offset, because
486
     * expected_size may be -1 if we do not know how large the file is,
487
     * in which case we should not adjust it. */
488
219
    if(data->state.resume_from <= expected_size)
489
79
      expected_size -= data->state.resume_from;
490
140
    else {
491
140
      failf(data, "failed to resume file:// transfer");
492
140
      return CURLE_BAD_DOWNLOAD_RESUME;
493
140
    }
494
219
  }
495
496
  /* A high water mark has been specified so we obey... */
497
916
  if(data->req.maxdownload > 0)
498
420
    expected_size = data->req.maxdownload;
499
500
916
  if(!fstated || (expected_size <= 0))
501
293
    size_known = FALSE;
502
623
  else
503
623
    size_known = TRUE;
504
505
  /* The following is a shortcut implementation of file reading
506
     this is both more efficient than the former call to download() and
507
     it avoids problems with select() and recv() on file descriptors
508
     in Winsock */
509
916
  if(size_known)
510
623
    Curl_pgrsSetDownloadSize(data, expected_size);
511
512
916
  if(data->state.resume_from) {
513
258
    if(!S_ISDIR(statbuf.st_mode)) {
514
136
      if(data->state.resume_from !=
515
136
         curl_lseek(fd, data->state.resume_from, SEEK_SET))
516
57
        return CURLE_BAD_DOWNLOAD_RESUME;
517
136
    }
518
122
    else {
519
122
      return CURLE_BAD_DOWNLOAD_RESUME;
520
122
    }
521
258
  }
522
523
737
  result = Curl_multi_xfer_buf_borrow(data, &xfer_buf, &xfer_blen);
524
737
  if(result)
525
0
    goto out;
526
527
737
  if(!S_ISDIR(statbuf.st_mode)) {
528
21.8k
    while(!result) {
529
21.8k
      ssize_t nread;
530
      /* Do not fill a whole buffer if we want less than all data */
531
21.8k
      size_t bytestoread;
532
533
21.8k
      if(size_known) {
534
21.7k
        bytestoread = (expected_size < (curl_off_t)(xfer_blen - 1)) ?
535
21.3k
          curlx_sotouz(expected_size) : (xfer_blen - 1);
536
21.7k
      }
537
88
      else
538
88
        bytestoread = xfer_blen - 1;
539
540
21.8k
      nread = read(fd, xfer_buf, bytestoread);
541
542
21.8k
      if(nread > 0)
543
21.5k
        xfer_buf[nread] = 0;
544
545
21.8k
      if(nread <= 0 || (size_known && (expected_size == 0)))
546
281
        break;
547
548
21.5k
      if(size_known)
549
21.4k
        expected_size -= nread;
550
551
21.5k
      result = Curl_client_write(data, CLIENTWRITE_BODY, xfer_buf, nread);
552
21.5k
      if(result)
553
28
        goto out;
554
555
21.5k
      result = Curl_pgrsCheck(data);
556
21.5k
      if(result)
557
0
        goto out;
558
21.5k
    }
559
309
  }
560
428
  else {
561
428
#ifdef HAVE_OPENDIR
562
428
    DIR *dir = opendir(file->path);
563
428
    struct dirent *entry;
564
565
428
    if(!dir) {
566
0
      result = CURLE_READ_ERROR;
567
0
      goto out;
568
0
    }
569
428
    else {
570
26.9k
      while((entry = readdir(dir))) {
571
26.5k
        if(entry->d_name[0] != '.') {
572
25.3k
          result = Curl_client_write(data, CLIENTWRITE_BODY,
573
25.3k
                   entry->d_name, strlen(entry->d_name));
574
25.3k
          if(result)
575
13
            break;
576
25.3k
          result = Curl_client_write(data, CLIENTWRITE_BODY, "\n", 1);
577
25.3k
          if(result)
578
3
            break;
579
25.3k
        }
580
26.5k
      }
581
428
      closedir(dir);
582
428
    }
583
#else
584
    failf(data, "Directory listing not yet implemented on this platform.");
585
    result = CURLE_READ_ERROR;
586
#endif
587
428
  }
588
589
709
  if(!result)
590
693
    result = Curl_pgrsUpdate(data);
591
592
737
out:
593
737
  Curl_multi_xfer_buf_release(data, xfer_buf);
594
737
  return result;
595
709
}
596
597
const struct Curl_protocol Curl_protocol_file = {
598
  file_setup_connection,                /* setup_connection */
599
  file_do,                              /* do_it */
600
  file_done,                            /* done */
601
  ZERO_NULL,                            /* do_more */
602
  file_connect,                         /* connect_it */
603
  ZERO_NULL,                            /* connecting */
604
  ZERO_NULL,                            /* doing */
605
  ZERO_NULL,                            /* proto_pollset */
606
  ZERO_NULL,                            /* doing_pollset */
607
  ZERO_NULL,                            /* domore_pollset */
608
  ZERO_NULL,                            /* perform_pollset */
609
  file_disconnect,                      /* disconnect */
610
  ZERO_NULL,                            /* write_resp */
611
  ZERO_NULL,                            /* write_resp_hd */
612
  ZERO_NULL,                            /* connection_is_dead */
613
  ZERO_NULL,                            /* attach connection */
614
  ZERO_NULL,                            /* follow */
615
};
616
617
#endif