Coverage Report

Created: 2026-08-13 07:18

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
0
#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
0
{
83
0
  curlx_safefree(file->freepath);
84
0
  file->path = NULL;
85
0
  if(file->fd != -1) {
86
0
    curlx_close(file->fd);
87
0
    file->fd = -1;
88
0
  }
89
0
}
90
91
static void file_easy_dtor(void *key, size_t klen, void *entry)
92
0
{
93
0
  struct FILEPROTO *file = entry;
94
0
  (void)key;
95
0
  (void)klen;
96
0
  file_cleanup(file);
97
0
  curlx_free(file);
98
0
}
99
100
static CURLcode file_setup_connection(struct Curl_easy *data,
101
                                      struct connectdata *conn)
102
0
{
103
0
  struct FILEPROTO *filep;
104
0
  (void)conn;
105
  /* allocate the FILE specific struct */
106
0
  filep = curlx_calloc(1, sizeof(*filep));
107
0
  if(filep)
108
0
    filep->fd = -1;
109
0
  if(!filep ||
110
0
     Curl_meta_set(data, CURL_META_FILE_EASY, filep, file_easy_dtor))
111
0
    return CURLE_OUT_OF_MEMORY;
112
113
0
  return CURLE_OK;
114
0
}
115
116
static CURLcode file_done(struct Curl_easy *data,
117
                          CURLcode status, bool premature)
118
0
{
119
0
  struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
120
0
  (void)status;
121
0
  (void)premature;
122
123
0
  if(file)
124
0
    file_cleanup(file);
125
126
0
  return CURLE_OK;
127
0
}
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
0
{
136
0
  char *real_path;
137
0
  struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
138
0
  int fd;
139
#ifdef DOS_FILESYSTEM
140
  size_t i;
141
  char *actual_path;
142
#endif
143
0
  size_t real_path_len;
144
0
  CURLcode result;
145
146
0
  if(!file)
147
0
    return CURLE_FAILED_INIT;
148
149
0
  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
0
    *done = TRUE;
155
0
    return CURLE_OK;
156
0
  }
157
158
0
  result = Curl_urldecode(data->state.up.path, 0, &real_path,
159
0
                          &real_path_len, REJECT_ZERO);
160
0
  if(result)
161
0
    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
0
  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
0
  fd = curlx_open(real_path, O_RDONLY);
229
0
  file->path = real_path;
230
0
#endif
231
0
#endif
232
0
  curlx_free(file->freepath);
233
0
  file->freepath = real_path; /* free this when done */
234
235
0
  file->fd = fd;
236
0
  if(!data->state.upload && (fd == -1)) {
237
0
    failf(data, "Could not open file %s", data->state.up.path);
238
0
    file_done(data, CURLE_FILE_COULDNT_READ_FILE, FALSE);
239
0
    return CURLE_FILE_COULDNT_READ_FILE;
240
0
  }
241
0
  *done = TRUE;
242
243
0
  return CURLE_OK;
244
0
}
245
246
static CURLcode file_disconnect(struct Curl_easy *data,
247
                                struct connectdata *conn,
248
                                bool dead_connection)
249
0
{
250
0
  (void)dead_connection;
251
0
  (void)conn;
252
0
  return file_done(data, CURLE_OK, FALSE);
253
0
}
254
255
#ifdef DOS_FILESYSTEM
256
#define DIRSEP '\\'
257
#else
258
0
#define DIRSEP '/'
259
#endif
260
261
static CURLcode file_upload(struct Curl_easy *data,
262
                            struct FILEPROTO *file)
263
0
{
264
0
  const char *dir = strchr(file->path, DIRSEP);
265
0
  int fd;
266
0
  int mode;
267
0
  CURLcode result = CURLE_OK;
268
0
  char *xfer_ulbuf;
269
0
  size_t xfer_ulblen;
270
0
  curlx_struct_stat file_stat;
271
0
  const char *sendbuf;
272
0
  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
0
  if(!dir)
280
0
    return CURLE_FILE_COULDNT_READ_FILE; /* fix: better error code */
281
282
0
  if(!dir[1])
283
0
    return CURLE_FILE_COULDNT_READ_FILE; /* fix: better error code */
284
285
0
  mode = O_WRONLY | O_CREAT | CURL_O_BINARY;
286
0
  if(data->state.resume_from)
287
0
    mode |= O_APPEND;
288
0
  else
289
0
    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
0
  fd = curlx_open(file->path, mode, data->set.new_file_perms);
299
0
#endif
300
0
  if(fd < 0) {
301
0
    failf(data, "cannot open %s for writing", file->path);
302
0
    return CURLE_WRITE_ERROR;
303
0
  }
304
305
0
  if(data->state.infilesize != -1)
306
    /* known size of data to "upload" */
307
0
    Curl_pgrsSetUploadSize(data, data->state.infilesize);
308
309
  /* treat the negative resume offset value as the case of "-" */
310
0
  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
0
  result = Curl_multi_xfer_ulbuf_borrow(data, &xfer_ulbuf, &xfer_ulblen);
320
0
  if(result)
321
0
    goto out;
322
323
0
  while(!result && !eos) {
324
0
    size_t nread, nwritten;
325
0
    ssize_t rv;
326
0
    size_t readcount;
327
328
0
    result = Curl_client_read(data, xfer_ulbuf, xfer_ulblen, &readcount, &eos);
329
0
    if(result)
330
0
      break;
331
332
0
    if(!readcount)
333
0
      break;
334
335
0
    nread = readcount;
336
337
    /* skip bytes before resume point */
338
0
    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
0
    else
351
0
      sendbuf = xfer_ulbuf;
352
353
    /* write the data to the target */
354
0
    rv = write(fd, sendbuf, nread);
355
0
    if(!curlx_sztouz(rv, &nwritten) || (nwritten != nread)) {
356
0
      result = CURLE_SEND_ERROR;
357
0
      break;
358
0
    }
359
0
    Curl_pgrs_upload_inc(data, nwritten);
360
361
0
    result = Curl_pgrsCheck(data);
362
0
  }
363
0
  if(!result)
364
0
    result = Curl_pgrsUpdate(data);
365
366
0
out:
367
0
  curlx_close(fd);
368
0
  Curl_multi_xfer_ulbuf_release(data, xfer_ulbuf);
369
370
0
  return result;
371
0
}
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
0
{
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
0
  struct FILEPROTO *file = Curl_meta_get(data, CURL_META_FILE_EASY);
388
0
  CURLcode result = CURLE_OK;
389
0
  curlx_struct_stat statbuf;
390
0
  curl_off_t expected_size = -1;
391
0
  bool size_known;
392
0
  bool fstated = FALSE;
393
0
  int fd;
394
0
  char *xfer_buf;
395
0
  size_t xfer_blen;
396
397
0
  *done = TRUE; /* unconditionally */
398
0
  if(!file)
399
0
    return CURLE_FAILED_INIT;
400
401
0
  if(data->state.upload)
402
0
    return file_upload(data, file);
403
404
  /* get the fd from the connection phase */
405
0
  fd = file->fd;
406
407
  /* VMS: This only works reliable for STREAMLF files */
408
0
  if(curlx_fstat(fd, &statbuf) != -1) {
409
0
    if(!S_ISDIR(statbuf.st_mode))
410
0
      expected_size = statbuf.st_size;
411
    /* and store the modification time */
412
0
    data->info.filetime = statbuf.st_mtime;
413
0
    fstated = TRUE;
414
0
  }
415
416
0
  if(fstated && !data->state.range && data->set.timecondition &&
417
0
     !Curl_meets_timecondition(data, data->info.filetime))
418
0
    return CURLE_OK;
419
420
0
  if(fstated) {
421
0
    time_t filetime;
422
0
    struct tm buffer;
423
0
    const struct tm *tm = &buffer;
424
0
    char header[80];
425
0
    int headerlen;
426
0
    static const char accept_ranges[] = { "Accept-ranges: bytes\r\n" };
427
0
    if(expected_size >= 0) {
428
0
      headerlen =
429
0
        curl_msnprintf(header, sizeof(header),
430
0
                       "Content-Length: %" FMT_OFF_T "\r\n", expected_size);
431
0
      result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
432
0
      if(result)
433
0
        return result;
434
435
0
      result = Curl_client_write(data, CLIENTWRITE_HEADER,
436
0
                                 accept_ranges, CURL_CSTRLEN(accept_ranges));
437
0
      if(result != CURLE_OK)
438
0
        return result;
439
0
    }
440
441
0
    filetime = (time_t)statbuf.st_mtime;
442
0
    result = curlx_gmtime(filetime, &buffer);
443
0
    if(result)
444
0
      return result;
445
446
    /* format: "Tue, 15 Nov 1994 12:45:26 GMT" */
447
0
    headerlen =
448
0
      curl_msnprintf(header, sizeof(header),
449
0
                     "Last-Modified: %s, %02d %s %4d %02d:%02d:%02d GMT\r\n",
450
0
                     Curl_wkday[tm->tm_wday ? tm->tm_wday - 1 : 6],
451
0
                     tm->tm_mday,
452
0
                     Curl_month[tm->tm_mon],
453
0
                     tm->tm_year + 1900,
454
0
                     tm->tm_hour,
455
0
                     tm->tm_min,
456
0
                     tm->tm_sec);
457
0
    result = Curl_client_write(data, CLIENTWRITE_HEADER, header, headerlen);
458
0
    if(!result)
459
      /* end of headers */
460
0
      result = Curl_client_write(data, CLIENTWRITE_HEADER, "\r\n", 2);
461
0
    if(result)
462
0
      return result;
463
    /* set the file size to make it available post transfer */
464
0
    Curl_pgrsSetDownloadSize(data, expected_size);
465
0
    if(data->req.no_body)
466
0
      return CURLE_OK;
467
0
  }
468
469
  /* Check whether file range has been specified */
470
0
  result = Curl_range(data);
471
0
  if(result)
472
0
    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
0
  if(data->state.resume_from < 0) {
477
0
    if(!fstated) {
478
0
      failf(data, "cannot get the size of file.");
479
0
      return CURLE_READ_ERROR;
480
0
    }
481
0
    data->state.resume_from += (curl_off_t)statbuf.st_size;
482
0
  }
483
484
0
  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
0
    if(data->state.resume_from <= expected_size)
489
0
      expected_size -= data->state.resume_from;
490
0
    else {
491
0
      failf(data, "failed to resume file:// transfer");
492
0
      return CURLE_BAD_DOWNLOAD_RESUME;
493
0
    }
494
0
  }
495
496
  /* A high water mark has been specified so we obey... */
497
0
  if(data->req.maxdownload > 0)
498
0
    expected_size = data->req.maxdownload;
499
500
0
  if(!fstated || (expected_size <= 0))
501
0
    size_known = FALSE;
502
0
  else
503
0
    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
0
  if(size_known)
510
0
    Curl_pgrsSetDownloadSize(data, expected_size);
511
512
0
  if(data->state.resume_from) {
513
0
    if(!S_ISDIR(statbuf.st_mode)) {
514
0
      if(data->state.resume_from !=
515
0
         curl_lseek(fd, data->state.resume_from, SEEK_SET))
516
0
        return CURLE_BAD_DOWNLOAD_RESUME;
517
0
    }
518
0
    else {
519
0
      return CURLE_BAD_DOWNLOAD_RESUME;
520
0
    }
521
0
  }
522
523
0
  result = Curl_multi_xfer_buf_borrow(data, &xfer_buf, &xfer_blen);
524
0
  if(result)
525
0
    goto out;
526
527
0
  if(!S_ISDIR(statbuf.st_mode)) {
528
0
    while(!result) {
529
0
      ssize_t nread;
530
      /* Do not fill a whole buffer if we want less than all data */
531
0
      size_t bytestoread;
532
533
0
      if(size_known) {
534
0
        bytestoread = (expected_size < (curl_off_t)(xfer_blen - 1)) ?
535
0
          curlx_sotouz(expected_size) : (xfer_blen - 1);
536
0
      }
537
0
      else
538
0
        bytestoread = xfer_blen - 1;
539
540
0
      nread = read(fd, xfer_buf, bytestoread);
541
542
0
      if(nread > 0)
543
0
        xfer_buf[nread] = 0;
544
545
0
      if(nread <= 0 || (size_known && (expected_size == 0)))
546
0
        break;
547
548
0
      if(size_known)
549
0
        expected_size -= nread;
550
551
0
      result = Curl_client_write(data, CLIENTWRITE_BODY, xfer_buf, nread);
552
0
      if(result)
553
0
        goto out;
554
555
0
      result = Curl_pgrsCheck(data);
556
0
      if(result)
557
0
        goto out;
558
0
    }
559
0
  }
560
0
  else {
561
0
#ifdef HAVE_OPENDIR
562
0
    DIR *dir = opendir(file->path);
563
0
    struct dirent *entry;
564
565
0
    if(!dir) {
566
0
      result = CURLE_READ_ERROR;
567
0
      goto out;
568
0
    }
569
0
    else {
570
0
      while((entry = readdir(dir))) {
571
0
        if(entry->d_name[0] != '.') {
572
0
          result = Curl_client_write(data, CLIENTWRITE_BODY,
573
0
                   entry->d_name, strlen(entry->d_name));
574
0
          if(result)
575
0
            break;
576
0
          result = Curl_client_write(data, CLIENTWRITE_BODY, "\n", 1);
577
0
          if(result)
578
0
            break;
579
0
        }
580
0
      }
581
0
      closedir(dir);
582
0
    }
583
#else
584
    failf(data, "Directory listing not yet implemented on this platform.");
585
    result = CURLE_READ_ERROR;
586
#endif
587
0
  }
588
589
0
  if(!result)
590
0
    result = Curl_pgrsUpdate(data);
591
592
0
out:
593
0
  Curl_multi_xfer_buf_release(data, xfer_buf);
594
0
  return result;
595
0
}
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