Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace Dotenv\Parser;
6
 
7
use Dotenv\Util\Regex;
8
use Dotenv\Util\Str;
9
use GrahamCampbell\ResultType\Error;
10
use GrahamCampbell\ResultType\Result;
11
use GrahamCampbell\ResultType\Success;
12
 
13
final class EntryParser
14
{
15
    private const INITIAL_STATE = 0;
16
    private const UNQUOTED_STATE = 1;
17
    private const SINGLE_QUOTED_STATE = 2;
18
    private const DOUBLE_QUOTED_STATE = 3;
19
    private const ESCAPE_SEQUENCE_STATE = 4;
20
    private const WHITESPACE_STATE = 5;
21
    private const COMMENT_STATE = 6;
22
    private const REJECT_STATES = [self::SINGLE_QUOTED_STATE, self::DOUBLE_QUOTED_STATE, self::ESCAPE_SEQUENCE_STATE];
23
 
24
    /**
25
     * This class is a singleton.
26
     *
27
     * @codeCoverageIgnore
28
     *
29
     * @return void
30
     */
31
    private function __construct()
32
    {
33
        //
34
    }
35
 
36
    /**
37
     * Parse a raw entry into a proper entry.
38
     *
39
     * That is, turn a raw environment variable entry into a name and possibly
40
     * a value. We wrap the answer in a result type.
41
     *
42
     * @param string $entry
43
     *
44
     * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry,string>
45
     */
46
    public static function parse(string $entry)
47
    {
48
        return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) {
49
            [$name, $value] = $parts;
50
 
51
            return self::parseName($name)->flatMap(static function (string $name) use ($value) {
52
                /** @var Result<Value|null,string> */
53
                $parsedValue = $value === null ? Success::create(null) : self::parseValue($value);
54
 
55
                return $parsedValue->map(static function (?Value $value) use ($name) {
56
                    return new Entry($name, $value);
57
                });
58
            });
59
        });
60
    }
61
 
62
    /**
63
     * Split the compound string into parts.
64
     *
65
     * @param string $line
66
     *
67
     * @return \GrahamCampbell\ResultType\Result<array{string,string|null},string>
68
     */
69
    private static function splitStringIntoParts(string $line)
70
    {
71
        /** @var array{string,string|null} */
72
        $result = Str::pos($line, '=')->map(static function () use ($line) {
73
            return \array_map('trim', \explode('=', $line, 2));
74
        })->getOrElse([$line, null]);
75
 
76
        if ($result[0] === '') {
77
            /** @var \GrahamCampbell\ResultType\Result<array{string,string|null},string> */
78
            return Error::create(self::getErrorMessage('an unexpected equals', $line));
79
        }
80
 
81
        /** @var \GrahamCampbell\ResultType\Result<array{string,string|null},string> */
82
        return Success::create($result);
83
    }
84
 
85
    /**
86
     * Parse the given variable name.
87
     *
88
     * That is, strip the optional quotes and leading "export" from the
89
     * variable name. We wrap the answer in a result type.
90
     *
91
     * @param string $name
92
     *
93
     * @return \GrahamCampbell\ResultType\Result<string,string>
94
     */
95
    private static function parseName(string $name)
96
    {
97
        if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) {
98
            $name = \ltrim(Str::substr($name, 6));
99
        }
100
 
101
        if (self::isQuotedName($name)) {
102
            $name = Str::substr($name, 1, -1);
103
        }
104
 
105
        if (!self::isValidName($name)) {
106
            /** @var \GrahamCampbell\ResultType\Result<string,string> */
107
            return Error::create(self::getErrorMessage('an invalid name', $name));
108
        }
109
 
110
        /** @var \GrahamCampbell\ResultType\Result<string,string> */
111
        return Success::create($name);
112
    }
113
 
114
    /**
115
     * Is the given variable name quoted?
116
     *
117
     * @param string $name
118
     *
119
     * @return bool
120
     */
121
    private static function isQuotedName(string $name)
122
    {
123
        if (Str::len($name) < 3) {
124
            return false;
125
        }
126
 
127
        $first = Str::substr($name, 0, 1);
128
        $last = Str::substr($name, -1, 1);
129
 
130
        return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\'');
131
    }
132
 
133
    /**
134
     * Is the given variable name valid?
135
     *
136
     * @param string $name
137
     *
138
     * @return bool
139
     */
140
    private static function isValidName(string $name)
141
    {
142
        return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false);
143
    }
144
 
145
    /**
146
     * Parse the given variable value.
147
     *
148
     * This has the effect of stripping quotes and comments, dealing with
149
     * special characters, and locating nested variables, but not resolving
150
     * them. Formally, we run a finite state automaton with an output tape: a
151
     * transducer. We wrap the answer in a result type.
152
     *
153
     * @param string $value
154
     *
155
     * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string>
156
     */
157
    private static function parseValue(string $value)
158
    {
159
        if (\trim($value) === '') {
160
            /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */
161
            return Success::create(Value::blank());
162
        }
163
 
164
        return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) {
165
            return $data->flatMap(static function (array $data) use ($token) {
166
                return self::processToken($data[1], $token)->map(static function (array $val) use ($data) {
167
                    return [$data[0]->append($val[0], $val[1]), $val[2]];
168
                });
169
            });
170
        }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) {
171
            /** @psalm-suppress DocblockTypeContradiction */
172
            if (in_array($result[1], self::REJECT_STATES, true)) {
173
                /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */
174
                return Error::create('a missing closing quote');
175
            }
176
 
177
            /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */
178
            return Success::create($result[0]);
179
        })->mapError(static function (string $err) use ($value) {
180
            return self::getErrorMessage($err, $value);
181
        });
182
    }
183
 
184
    /**
185
     * Process the given token.
186
     *
187
     * @param int    $state
188
     * @param string $token
189
     *
190
     * @return \GrahamCampbell\ResultType\Result<array{string,bool,int},string>
191
     */
192
    private static function processToken(int $state, string $token)
193
    {
194
        switch ($state) {
195
            case self::INITIAL_STATE:
196
                if ($token === '\'') {
197
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
198
                    return Success::create(['', false, self::SINGLE_QUOTED_STATE]);
199
                } elseif ($token === '"') {
200
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
201
                    return Success::create(['', false, self::DOUBLE_QUOTED_STATE]);
202
                } elseif ($token === '#') {
203
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
204
                    return Success::create(['', false, self::COMMENT_STATE]);
205
                } elseif ($token === '$') {
206
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
207
                    return Success::create([$token, true, self::UNQUOTED_STATE]);
208
                } else {
209
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
210
                    return Success::create([$token, false, self::UNQUOTED_STATE]);
211
                }
212
            case self::UNQUOTED_STATE:
213
                if ($token === '#') {
214
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
215
                    return Success::create(['', false, self::COMMENT_STATE]);
216
                } elseif (\ctype_space($token)) {
217
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
218
                    return Success::create(['', false, self::WHITESPACE_STATE]);
219
                } elseif ($token === '$') {
220
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
221
                    return Success::create([$token, true, self::UNQUOTED_STATE]);
222
                } else {
223
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
224
                    return Success::create([$token, false, self::UNQUOTED_STATE]);
225
                }
226
            case self::SINGLE_QUOTED_STATE:
227
                if ($token === '\'') {
228
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
229
                    return Success::create(['', false, self::WHITESPACE_STATE]);
230
                } else {
231
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
232
                    return Success::create([$token, false, self::SINGLE_QUOTED_STATE]);
233
                }
234
            case self::DOUBLE_QUOTED_STATE:
235
                if ($token === '"') {
236
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
237
                    return Success::create(['', false, self::WHITESPACE_STATE]);
238
                } elseif ($token === '\\') {
239
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
240
                    return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]);
241
                } elseif ($token === '$') {
242
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
243
                    return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]);
244
                } else {
245
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
246
                    return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
247
                }
248
            case self::ESCAPE_SEQUENCE_STATE:
249
                if ($token === '"' || $token === '\\') {
250
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
251
                    return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
252
                } elseif ($token === '$') {
253
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
254
                    return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
255
                } else {
256
                    $first = Str::substr($token, 0, 1);
257
                    if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) {
258
                        /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
259
                        return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]);
260
                    } else {
261
                        /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
262
                        return Error::create('an unexpected escape sequence');
263
                    }
264
                }
265
            case self::WHITESPACE_STATE:
266
                if ($token === '#') {
267
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
268
                    return Success::create(['', false, self::COMMENT_STATE]);
269
                } elseif (!\ctype_space($token)) {
270
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
271
                    return Error::create('unexpected whitespace');
272
                } else {
273
                    /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
274
                    return Success::create(['', false, self::WHITESPACE_STATE]);
275
                }
276
            case self::COMMENT_STATE:
277
                /** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
278
                return Success::create(['', false, self::COMMENT_STATE]);
279
            default:
280
                throw new \Error('Parser entered invalid state.');
281
        }
282
    }
283
 
284
    /**
285
     * Generate a friendly error message.
286
     *
287
     * @param string $cause
288
     * @param string $subject
289
     *
290
     * @return string
291
     */
292
    private static function getErrorMessage(string $cause, string $subject)
293
    {
294
        return \sprintf(
295
            'Encountered %s at [%s].',
296
            $cause,
297
            \strtok($subject, "\n")
298
        );
299
    }
300
}