Issues (12)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Application.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Consolidation\Cgr;
4
5
/**
6
 * Note that this command is deliberately written using only php-native
7
 * libraries, and no external dependencies whatsoever, so that it may
8
 * be installed via `composer global require` without causing any conflicts
9
 * with any other project.
10
 *
11
 * This technique is NOT recommended for other tools. Use Symfony Console
12
 * directly, or, better yet, use Robo (http://robo.li) as a framework.
13
 * See: http://robo.li/framework/
14
 */
15
class Application
16
{
17
    protected $outputFile = '';
18
19
    /**
20
     * Run the cgr tool, a safer alternative to `composer global require`.
21
     *
22
     * @param array $argv The global $argv array passed in by PHP
23
     * @param string $home The path to the composer home directory
24
     * @return integer
25
     */
26
    public function run($argv, $home)
27
    {
28
        $optionDefaultValues = $this->getDefaultOptionValues($home);
29
        $optionDefaultValues = $this->overlayEnvironmentValues($optionDefaultValues);
30
31
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
32
33
        $helpArg = $this->getHelpArgValue($argv);
34
        if (!empty($helpArg)) {
35
            return $this->help($helpArg);
36
        }
37
38
        $commandList = $this->separateProjectAndGetCommandList($argv, $home, $options);
39
        if (empty($commandList)) {
40
            return 1;
41
        }
42
        return $this->runCommandList($commandList, $options);
43
    }
44
45
    /**
46
     * Returns the first argument after `help`, or the
47
     * first argument if `--help` is present. Otherwise,
48
     * returns an empty string.
49
     */
50
    public function getHelpArgValue($argv)
51
    {
52
        $hasHelp = false;
53
        $helpArg = '';
54
55
        foreach ($argv as $arg) {
56
            if (($arg == 'help') || ($arg == '--help') || ($arg == '-h')) {
57
                $hasHelp = true;
58
            } elseif (($arg[0] != '-') && empty($helpArg)) {
59
                $helpArg = $arg;
60
            }
61
        }
62
63
        if (!$hasHelp) {
64
            return false;
65
        }
66
67
        if (empty($helpArg)) {
68
            return 'help';
69
        }
70
71
        return $helpArg;
72
    }
73
74
    public function help($helpArg)
75
    {
76
        $helpFile = dirname(__DIR__) . '/help/' . $helpArg;
77
78
        if (!file_exists($helpFile)) {
79
            print "No help available for '$helpArg'\n";
80
            return 1;
81
        }
82
83
        $helpContents = file_get_contents($helpFile);
84
        print $helpContents;
85
        return 0;
86
    }
87
88
    /**
89
     * Set up output redirection. Used by tests.
90
     */
91
    public function setOutputFile($outputFile)
92
    {
93
        $this->outputFile = $outputFile;
94
    }
95
96
    /**
97
     * Figure out everything we're going to do, but don't do any of it
98
     * yet, just return the command objects to run.
99
     */
100
    public function parseArgvAndGetCommandList($argv, $home)
101
    {
102
        $optionDefaultValues = $this->getDefaultOptionValues($home);
103
        $optionDefaultValues = $this->overlayEnvironmentValues($optionDefaultValues);
104
105
        list($argv, $options) = $this->parseOutOurOptions($argv, $optionDefaultValues);
106
        return $this->separateProjectAndGetCommandList($argv, $home, $options);
107
    }
108
109
    /**
110
     * Figure out everything we're going to do, but don't do any of it
111
     * yet, just return the command objects to run.
112
     */
113
    public function separateProjectAndGetCommandList($argv, $home, $options)
114
    {
115
        list($command, $projects, $composerArgs) = $this->separateProjectsFromArgs($argv, $options);
116
117
        // If command was unknown, then exit with an error message
118
        if (empty($command)) {
119
            print "Unknown command: " . implode(' ', $composerArgs) . "\n";
120
            exit(1);
121
        }
122
123
        $commandList = $this->getCommandsToExec($command, $composerArgs, $projects, $options);
124
        return $commandList;
125
    }
126
127
    /**
128
     * Run all of the commands in a list.  Abort early if any fail.
129
     *
130
     * @param array $commandList An array of CommandToExec
131
     * @return integer
132
     */
133
    public function runCommandList($commandList, $options)
134
    {
135
        foreach ($commandList as $command) {
136
            $exitCode = $command->run($this->outputFile);
137
            if ($exitCode) {
138
                return $exitCode;
139
            }
140
        }
141
        return 0;
142
    }
143
144
    /**
145
     * Return an array containing a list of commands to execute.  Depending on
146
     * the composition of the aguments and projects parameters, this list will
147
     * contain either a single command string to call through to composer (if
148
     * cgr is being used as a composer alias), or it will contain a list of
149
     * appropriate replacement 'composer global require' commands that install
150
     * each project in its own installation directory, while installing each
151
     * projects' binaries in the global Composer bin directory,
152
     * ~/.composer/vendor/bin.
153
     *
154
     * @param array $composerArgs
155
     * @param array $projects
156
     * @param array $options
157
     * @return CommandToExec
158
     */
159
    public function getCommandsToExec($command, $composerArgs, $projects, $options)
160
    {
161
        $execPath = $options['composer-path'];
162
163
        // Call requireCommand, updateCommand, or removeCommand, as appropriate.
164
        $methodName = "{$command}Command";
165
        if (method_exists($this, $methodName)) {
166
            return $this->$methodName($execPath, $composerArgs, $projects, $options);
167
        } else {
168
            // If there is no specific implementation for the requested command, then call 'generalCommand'.
169
            return $this->generalCommand($command, $execPath, $composerArgs, $projects, $options);
170
        }
171
    }
172
173
    /**
174
     * Return our list of default option values, with paths relative to
175
     * the provided home directory.
176
     * @param string $home The composer home directory
177
     * @return array
178
     */
179
    public function getDefaultOptionValues($home)
180
    {
181
        return array(
182
            'composer' => false,
183
            'composer-path' => 'composer',
184
            'base-dir' => "$home/global",
185
            'bin-dir' => "$home/vendor/bin",
186
            'stability' => false,
187
        );
188
    }
189
190
    /**
191
     * Replace option default values with the corresponding
192
     * environment variable value, if it is set.
193
     */
194
    protected function overlayEnvironmentValues($defaults)
195
    {
196
        foreach ($defaults as $key => $value) {
197
            $envKey = 'CGR_' . strtoupper(strtr($key, '-', '_'));
198
            $envValue = getenv($envKey);
199
            if ($envValue) {
200
                $defaults[$key] = $envValue;
201
            }
202
        }
203
        return $defaults;
204
    }
205
206
    /**
207
     * We use our own special-purpose argv parser. The options that apply
208
     * to this tool are identified by a simple associative array, where
209
     * the key is the option name, and the value is its default value.
210
     * The result of this function is an array of two items containing:
211
     *  - An array of the items in $argv not used to set an option value
212
     *  - An array of options containing the user-specified or default values
213
     *
214
     * @param array $argv The global $argv passed in by php
215
     * @param array $optionDefaultValues An associative array
216
     * @return array
217
     */
218
    public function parseOutOurOptions($argv, $optionDefaultValues)
219
    {
220
        $argv0 = array_shift($argv);
221
        $options['composer'] = (strpos($argv0, 'composer') !== false);
222
        $passAlongArgvItems = array();
223
        $options = array();
224
        while (!empty($argv)) {
225
            $arg = array_shift($argv);
226
            if ((substr($arg, 0, 2) == '--') && array_key_exists(substr($arg, 2), $optionDefaultValues)) {
227
                $options[substr($arg, 2)] = array_shift($argv);
228
            } else {
229
                $passAlongArgvItems[] = $arg;
230
            }
231
        }
232
        return array($passAlongArgvItems, $options + $optionDefaultValues);
233
    }
234
235
    /**
236
     * After our options are removed by parseOutOurOptions, those items remaining
237
     * in $argv will be separated into a list of projects and versions, and
238
     * anything else that is not a project:version. Returns an array of two
239
     * items containing:
240
     *  - An associative array, where the key is the project name and the value
241
     *    is the version (or an empty string, if no version was specified)
242
     *  - The remaining $argv items not used to build the projects array.
243
     *
244
     * @param array $argv The $argv array from parseOutOurOptions()
245
     * @return array
246
     */
247
    public function separateProjectsFromArgs($argv, $options)
248
    {
249
        $cgrCommands = array('info', 'require', 'update', 'remove', 'extend');
250
        $command = 'require';
251
        $composerArgs = array();
252
        $projects = array();
253
        $globalMode = !$options['composer'];
254
        foreach ($argv as $arg) {
255
            if ($arg[0] == '-') {
256
                // Any flags (first character is '-') will just be passed
257
                // through to to composer. Flags interpreted by cgr have
258
                // already been removed from $argv.
259
                $composerArgs[] = $arg;
260
            } elseif (strpos($arg, '/') !== false) {
261
                // Arguments containing a '/' name projects.  We will split
262
                // the project from its version, allowing the separator
263
                // character to be either a '=' or a ':', and then store the
264
                // result in the $projects array.
265
                $projectAndVersion = explode(':', strtr($arg, '=', ':'), 2) + array('', '');
266
                list($project, $version) = $projectAndVersion;
267
                $projects[$project] = $version;
268
            } elseif ($this->isComposerVersion($arg)) {
269
                // If an argument is a composer version, then we will alter
270
                // the last project we saw, attaching this version to it.
271
                // This allows us to handle 'a/b:1.0' and 'a/b 1.0' equivalently.
272
                $keys = array_keys($projects);
273
                $lastProject = array_pop($keys);
274
                unset($projects[$lastProject]);
275
                $projects[$lastProject] = $arg;
276
            } elseif ($arg == 'global') {
277
                // Make note if we see the 'global' command.
278
                $globalMode = true;
279
            } elseif ($command == 'extend') {
280
                $projects[$arg] = true;
281
            } else {
282
                // If we see any command other than 'global [require|update|remove]',
283
                // then we will pass *all* of the arguments through to
284
                // composer unchanged. We return an empty projects array
285
                // to indicate that this should be a pass-through call
286
                // to composer, rather than one or more calls to
287
                // 'composer require' to install global projects.
288
                if ((!$globalMode) || (!in_array($arg, $cgrCommands))) {
289
                    return array('', array(), $argv);
290
                }
291
                // Remember which command we saw
292
                $command = $arg;
293
            }
294
        }
295
        return array($command, $projects, $composerArgs);
296
    }
297
298
    /**
299
     * Provide a safer version of `composer global require`.  Each project
300
     * listed in $projects will be installed into its own project directory.
301
     * The binaries from each project will still be placed in the global
302
     * composer bin directory.
303
     *
304
     * @param string $execPath The path to composer
305
     * @param array $composerArgs Anything from the global $argv to be passed
306
     *   on to Composer
307
     * @param array $projects A list of projects to install, with the key
308
     *   specifying the project name, and the value specifying its version.
309
     * @param array $options User options from the command line; see
310
     *   $optionDefaultValues in the main() function.
311
     * @return array
312
     */
313
    public function requireCommand($execPath, $composerArgs, $projects, $options)
314
    {
315
        $stabilityCommands = array();
316
        if ($options['stability']) {
317
            $stabilityCommands = $this->configureProjectStability($execPath, $composerArgs, $projects, $options);
318
        }
319
        $requireCommands = $this->generalCommand('require', $execPath, $composerArgs, $projects, $options);
320
        return array_merge($stabilityCommands, $requireCommands);
321
    }
322
323
    public function extendCommand($execPath, $composerArgs, $projects, $options)
324
    {
325
        $projectToExtend = $this->getProjectToExtend($projects);
326
        if (!$projectToExtend) {
327
            print "No command to extend specified\n";
328
            exit(1);
329
        }
330
        array_shift($projects);
331
332
        $options['base-dir'] .= '/' . $projectToExtend;
333
        $options['extend-mode'] = true;
334
        if (!is_dir($options['base-dir'])) {
335
            print "Project $projectToExtend not found; try 'cgr require' first\n";
336
            exit(1);
337
        }
338
339
        return $this->requireCommand($execPath, $composerArgs, $projects, $options);
340
    }
341
342
    protected function getProjectToExtend($projects)
343
    {
344
        $keys = array_keys($projects);
345
        $project = array_shift($keys);
346
347
        return $project;
348
    }
349
350
    /**
351
     * General command handler.
352
     *
353
     * @param string $composerCommand The composer command to run e.g. require
354
     * @param string $execPath The path to composer
355
     * @param array $composerArgs Anything from the global $argv to be passed
356
     *   on to Composer
357
     * @param array $projects A list of projects to install, with the key
358
     *   specifying the project name, and the value specifying its version.
359
     * @param array $options User options from the command line; see
360
     *   $optionDefaultValues in the main() function.
361
     * @return array
362
     */
363
    public function generalCommand($composerCommand, $execPath, $composerArgs, $projects, $options)
364
    {
365
        $globalBaseDir = $options['base-dir'];
366
        $binDir = $options['bin-dir'];
367
        $extendMode = !empty($options['extend-mode']);
368
        $env = array("COMPOSER_BIN_DIR" => $binDir);
369
        $result = array();
370
        foreach ($projects as $project => $version) {
371
            $installLocation = $extendMode ? $globalBaseDir : "$globalBaseDir/$project";
372
            $projectWithVersion = $this->projectWithVersion($project, $version);
373
            $commandToExec = $this->buildGlobalCommand($composerCommand, $execPath, $composerArgs, $projectWithVersion, $env, $installLocation);
374
            $result[] = $commandToExec;
375
        }
376
        return $result;
377
    }
378
379
    /**
380
     * Remove command handler. Build an `rm -rf` command.
381
     *
382
     * @param string $execPath The path to composer (ignored)
383
     * @param array $composerArgs Anything from the global $argv to be passed
384
     *   on to Composer (ignored)
385
     * @param array $projects A list of projects to install, with the key
386
     *   specifying the project name, and the value specifying its version.
387
     * @param array $options User options from the command line; see
388
     *   $optionDefaultValues in the main() function.
389
     * @return array
390
     */
391
    public function removeCommand($execPath, $composerArgs, $projects, $options)
392
    {
393
        $globalBaseDir = $options['base-dir'];
394
        $env = array();
395
        $result = $this->generalCommand('remove', $execPath, $composerArgs, $projects, $options);
396
        foreach ($projects as $project => $version) {
397
            $installLocation = "$globalBaseDir/$project";
398
            $result[] = new CommandToExec('rm', array('-rf', $installLocation), $env, $installLocation);
399
        }
400
        return $result;
401
    }
402
403
    /**
404
     * Command handler for commands where the project should not be provided
405
     * as a parameter to Composer (e.g. 'update').
406
     *
407
     * @param string $composerCommand The composer command to run e.g. require
408
     * @param string $execPath The path to composer
409
     * @param array $composerArgs Anything from the global $argv to be passed
410
     *   on to Composer
411
     * @param array $projects A list of projects to install, with the key
412
     *   specifying the project name, and the value specifying its version.
413
     * @param array $options User options from the command line; see
414
     *   $optionDefaultValues in the main() function.
415
     * @return array
416
     */
417
    public function noProjectArgCommand($composerCommand, $execPath, $composerArgs, $projects, $options)
418
    {
419
        $globalBaseDir = $options['base-dir'];
420
        $binDir = $options['bin-dir'];
421
        $env = array("COMPOSER_BIN_DIR" => $binDir);
422
        $result = array();
423
        foreach ($projects as $project => $version) {
424
            $installLocation = "$globalBaseDir/$project";
425
            $commandToExec = $this->buildGlobalCommand($composerCommand, $execPath, $composerArgs, '', $env, $installLocation);
426
            $result[] = $commandToExec;
427
        }
428
        return $result;
429
    }
430
431
    /**
432
     * If --stability VALUE is provided, then run a `composer config minimum-stability VALUE`
433
     * command to configure composer.json appropriately.
434
     *
435
     * @param string $execPath The path to composer
436
     * @param array $composerArgs Anything from the global $argv to be passed
437
     *   on to Composer
438
     * @param array $projects A list of projects to install, with the key
439
     *   specifying the project name, and the value specifying its version.
440
     * @param array $options User options from the command line; see
441
     *   $optionDefaultValues in the main() function.
442
     * @return array
443
     */
444
    public function configureProjectStability($execPath, $composerArgs, $projects, $options)
445
    {
446
        $globalBaseDir = $options['base-dir'];
447
        $stability = $options['stability'];
448
        $result = array();
449
        $env = array();
450
451
        foreach ($projects as $project => $version) {
452
            $installLocation = "$globalBaseDir/$project";
453
            FileSystemUtils::mkdirParents($installLocation);
454
            if (!file_exists("$installLocation/composer.json")) {
455
                file_put_contents("$installLocation/composer.json", '{}');
456
            }
457
            $result[] = $this->buildConfigCommand($execPath, $composerArgs, 'minimum-stability', $stability, $env, $installLocation);
458
        }
459
        return $result;
460
    }
461
462
    /**
463
     * Run `composer info`. Not only do we want to display the information of
464
     * the "global" Composer project, we also want to get the infomation of
465
     * all the "isolated" projects installed via cgr in ~/.composer/global.
466
     *
467
     * @param string $command The path to composer
0 ignored issues
show
There is no parameter named $command. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
468
     * @param array $composerArgs Anything from the global $argv to be passed
469
     *   on to Composer
470
     * @param array $projects A list of projects to update.
471
     * @param array $options User options from the command line; see
472
     *   $optionDefaultValues in the main() function.
473
     * @return array
474
     */
475 View Code Duplication
    public function infoCommand($execPath, $composerArgs, $projects, $options)
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
476
    {
477
        // If 'projects' list is empty, make a list of everything currently installed
478
        if (empty($projects)) {
479
            $projects = FileSystemUtils::allInstalledProjectsInBaseDir($options['base-dir']);
480
            $projects = $this->flipProjectsArray($projects);
481
        }
482
        return $this->generalCommand('info', $execPath, $composerArgs, $projects, $options);
483
    }
484
485
    /**
486
     * Run `composer global update`. Not only do we want to update the
487
     * "global" Composer project, we also want to update all of the
488
     * "isolated" projects installed via cgr in ~/.composer/global.
489
     *
490
     * @param string $command The path to composer
491
     * @param array $composerArgs Anything from the global $argv to be passed
492
     *   on to Composer
493
     * @param array $projects A list of projects to update.
494
     * @param array $options User options from the command line; see
495
     *   $optionDefaultValues in the main() function.
496
     * @return array
497
     */
498 View Code Duplication
    public function updateCommand($execPath, $composerArgs, $projects, $options)
499
    {
500
        // If 'projects' list is empty, make a list of everything currently installed
501
        if (empty($projects)) {
502
            $projects = FileSystemUtils::allInstalledProjectsInBaseDir($options['base-dir']);
503
            $projects = $this->flipProjectsArray($projects);
504
        }
505
        return $this->noProjectArgCommand('update', $execPath, $composerArgs, $projects, $options);
506
    }
507
508
    /**
509
     * Convert from an array of projects to an array where the key is the
510
     * project name, and the value (version) is an empty string.
511
     *
512
     * @param string[] $projects
513
     * @return array
514
     */
515
    public function flipProjectsArray($projects)
516
    {
517
        return array_map(function () {
518
            return '';
519
        }, array_flip($projects));
520
    }
521
522
    /**
523
     * Return $project:$version, or just $project if there is no $version.
524
     *
525
     * @param string $project The project to install
526
     * @param string $version The version desired
527
     * @return string
528
     */
529
    public function projectWithVersion($project, $version)
530
    {
531
        if (empty($version)) {
532
            return $project;
533
        }
534
        return "$project:$version";
535
    }
536
537
    /**
538
     * Generate command string to call `composer COMMAND` to install one project.
539
     *
540
     * @param string $command The path to composer
541
     * @param array $composerArgs The arguments to pass to composer
542
     * @param string $projectWithVersion The project:version to install
543
     * @param array $env Environment to set prior to exec
544
     * @param string $installLocation Location to install the project
545
     * @return CommandToExec
546
     */
547
    public function buildGlobalCommand($composerCommand, $execPath, $composerArgs, $projectWithVersion, $env, $installLocation)
548
    {
549
        $projectSpecificArgs = array("--working-dir=$installLocation", $composerCommand);
550
        if (!empty($projectWithVersion)) {
551
            $projectSpecificArgs[] = $projectWithVersion;
552
        }
553
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
554
        return new CommandToExec($execPath, $arguments, $env, $installLocation);
555
    }
556
557
    /**
558
     * Generate command string to call `composer config KEY VALUE` to install one project.
559
     *
560
     * @param string $execPath The path to composer
561
     * @param array $composerArgs The arguments to pass to composer
562
     * @param string $key The config item to set
563
     * @param string $value The value to set the config item to
564
     * @param array $env Environment to set prior to exec
565
     * @param string $installLocation Location to install the project
566
     * @return CommandToExec
567
     */
568
    public function buildConfigCommand($execPath, $composerArgs, $key, $value, $env, $installLocation)
569
    {
570
        $projectSpecificArgs = array("--working-dir=$installLocation", 'config', $key, $value);
571
        $arguments = array_merge($composerArgs, $projectSpecificArgs);
572
        return new CommandToExec($execPath, $arguments, $env, $installLocation);
573
    }
574
575
    /**
576
     * Identify an argument that could be a Composer version string.
577
     *
578
     * @param string $arg The argument to test
579
     * @return boolean
580
     */
581
    public function isComposerVersion($arg)
582
    {
583
        // Allow for 'dev-master', et. al.
584
        if (substr($arg, 0, 4) == 'dev-') {
585
            return true;
586
        }
587
        $specialVersionChars = array('^', '~', '<', '>');
588
        return is_numeric($arg[0]) || in_array($arg[0], $specialVersionChars);
589
    }
590
}
591