Coverage Report

Created: 2026-05-30 06:06

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