summaryrefslogtreecommitdiffstats
path: root/lib/areadlinkat-with-size.c
blob: 6f015b4994f7c98ca0c12a65aeb45606e2196373 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/* readlinkat wrapper to return the link name in malloc'd storage.
   Unlike xreadlinkat, only call exit on failure to change directory.

   Copyright (C) 2001, 2003-2007, 2009-2022 Free Software Foundation, Inc.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

/* Written by Jim Meyering <jim@meyering.net>
   and Eric Blake <ebb9@byu.net>.  */

#include <config.h>

#include "areadlink.h"

#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#if HAVE_READLINKAT

# ifndef SSIZE_MAX
#  define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2))
# endif

/* SYMLINK_MAX is used only for an initial memory-allocation sanity
   check, so it's OK to guess too small on hosts where there is no
   arbitrary limit to symbolic link length.  */
# ifndef SYMLINK_MAX
#  define SYMLINK_MAX 1024
# endif

# define MAXSIZE (SIZE_MAX < SSIZE_MAX ? SIZE_MAX : SSIZE_MAX)

/* Call readlinkat to get the symbolic link value of FILE, relative to FD.
   SIZE is a hint as to how long the link is expected to be;
   typically it is taken from st_size.  It need not be correct.
   Return a pointer to that NUL-terminated string in malloc'd storage.
   If readlinkat fails, malloc fails, or if the link value is longer
   than SSIZE_MAX, return NULL (caller may use errno to diagnose).
   However, failure to change directory during readlinkat will issue
   a diagnostic and exit.  */

char *
areadlinkat_with_size (int fd, char const *file, size_t size)
{
  /* Some buggy file systems report garbage in st_size.  Defend
     against them by ignoring outlandish st_size values in the initial
     memory allocation.  */
  size_t symlink_max = SYMLINK_MAX;
  size_t INITIAL_LIMIT_BOUND = 8 * 1024;
  size_t initial_limit = (symlink_max < INITIAL_LIMIT_BOUND
                          ? symlink_max + 1
                          : INITIAL_LIMIT_BOUND);

  enum { stackbuf_size = 128 };

  /* The initial buffer size for the link value.  */
  size_t buf_size = (size == 0 ? stackbuf_size
                     : size < initial_limit ? size + 1 : initial_limit);

  while (1)
    {
      ssize_t r;
      size_t link_length;
      char stackbuf[stackbuf_size];
      char *buf = stackbuf;
      char *buffer = NULL;

      if (! (size == 0 && buf_size == stackbuf_size))
        {
          buf = buffer = malloc (buf_size);
          if (!buffer)
            /* We can assume errno == ENOMEM here, since all platforms that have
               readlinkat() have a POSIX compliant malloc().  */
            return NULL;
        }

      r = readlinkat (fd, file, buf, buf_size);
      link_length = r;

      if (r < 0)
        {
          free (buffer);
          return NULL;
        }

      if (link_length < buf_size)
        {
          buf[link_length] = 0;
          if (!buffer)
            {
              buffer = malloc (link_length + 1);
              if (buffer)
                return memcpy (buffer, buf, link_length + 1);
            }
          else if (link_length + 1 < buf_size)
            {
              /* Shrink BUFFER before returning it.  */
              char *shrinked_buffer = realloc (buffer, link_length + 1);
              if (shrinked_buffer != NULL)
                buffer = shrinked_buffer;
            }
          return buffer;
        }

      free (buffer);
      if (buf_size <= MAXSIZE / 2)
        buf_size *= 2;
      else if (buf_size < MAXSIZE)
        buf_size = MAXSIZE;
      else
        {
          errno = ENOMEM;
          return NULL;
        }
    }
}

#else /* !HAVE_READLINKAT */


/* It is more efficient to change directories only once and call
   areadlink_with_size, rather than repeatedly call the replacement
   readlinkat.  */

# define AT_FUNC_NAME areadlinkat_with_size
# define AT_FUNC_F1 areadlink_with_size
# define AT_FUNC_POST_FILE_PARAM_DECLS , size_t size
# define AT_FUNC_POST_FILE_ARGS        , size
# define AT_FUNC_RESULT char *
# define AT_FUNC_FAIL NULL
# include "at-func.c"
# undef AT_FUNC_NAME
# undef AT_FUNC_F1
# undef AT_FUNC_POST_FILE_PARAM_DECLS
# undef AT_FUNC_POST_FILE_ARGS
# undef AT_FUNC_RESULT
# undef AT_FUNC_FAIL

#endif /* !HAVE_READLINKAT */