Repository: serverless/serverless (JavaScript)
Date: 2018-11-15 16:35:07+00:00
Task type: Bug Fix

Issue:
base64 encoding: Serverless command "LS12ZXJib3Nl" not found
<!--
1. If you have a question and not a bug report please ask first at http://forum.serverless.com
2. Please check if an issue already exists. This bug may have already been documented
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

The following error:

```
Serverless command "ZHluYW1vZGI=" not found. Did you mean "plugin"? Run "serverless help" for a list of all available commands.
```

happens when `serverless --service=foo dynamodb install` is called.

It can also be reproduced with a simpler command like `serverless --no-color --verbose info`, which outputs:
```
Serverless command "LS12ZXJib3Nl" not found. Did you mean "config"? Run "serverless help" for a list of all available commands.
```
## Additional Data

It happens only with the `1.33.0` version, which was released today. It is caused by the changes from https://github.com/serverless/serverless/pull/5361


Issue discussion:

Solution patch:
diff --git a/lib/classes/CLI.js b/lib/classes/CLI.js
index f4db3010393..cf5972afce8 100644
--- a/lib/classes/CLI.js
+++ b/lib/classes/CLI.js
@@ -34,32 +34,43 @@ class CLI {
       inputArray = process.argv.slice(2);
     }
 
-    const toBase64Helper = (value, index) => {
-      if ((index % 2) !== 0) {
-        return new Buffer(value.toString()).toString('base64');
+    const base64Encode = (valueStr) =>
+      new Buffer(valueStr).toString('base64');
+
+    const toBase64Helper = (value) => {
+      const valueStr = value.toString();
+      if (valueStr.startsWith('-')) {
+        if (valueStr.indexOf('=') !== -1) {
+          // do not encode argument names, since those are parsed by
+          // minimist, and thus need to be there unconverted:
+          const splitted = valueStr.split('=', 2);
+          // splitted[1] values, however, need to be encoded, since we
+          // decode them later back to utf8
+          const encodedValue = base64Encode(splitted[1]);
+          return `${splitted[0]}=${encodedValue}`;
+        }
+        // do not encode plain flags, for the same reason as above
+        return valueStr;
       }
-      return value;
+      return base64Encode(valueStr);
     };
 
     const decodedArgsHelper = (arg) => {
       if (_.isString(arg)) {
         return new Buffer(arg, 'base64').toString();
+      } else if (_.isArray(arg)) {
+        return _.map(arg, decodedArgsHelper);
       }
       return arg;
     };
 
-    // get all the commands
-    const valuesToIgnore = _.takeWhile(inputArray, (item) => !item.startsWith('-'));
-    // get all the options
-    const valuesToConvert = _.difference(inputArray, valuesToIgnore);
     // encode all the options values to base64
-    const valuesToB64 = _.map(valuesToConvert, toBase64Helper);
-    // concat commands with values on base64
-    const valuesToParse = _.concat(valuesToIgnore, valuesToB64);
+    const valuesToParse = _.map(inputArray, toBase64Helper);
 
+    // parse the options with minimist
     const argvToParse = minimist(valuesToParse);
 
-    // decode all values to utf8 strings
+    // decode all values back to utf8 strings
     const argv = _.mapValues(argvToParse, decodedArgsHelper);
 
     const commands = [].concat(argv._);


Test patch:
diff --git a/lib/classes/CLI.test.js b/lib/classes/CLI.test.js
index 112b8befaba..30237c50826 100644
--- a/lib/classes/CLI.test.js
+++ b/lib/classes/CLI.test.js
@@ -371,6 +371,29 @@ describe('CLI', () => {
       expect(inputToBeProcessed).to.deep.equal(expectedObject);
     });
 
+    it('should not pass base64 values as options', () => {
+      cli = new CLI(serverless, ['--service=foo', 'dynamodb', 'install']);
+      const inputToBeProcessed = cli.processInput();
+
+      /* It used to fail with the following diff, failing to convert base64 back,
+         and unconverting non-base64 values into binary:
+       {
+         "commands": [
+      -    "ZHluYW1vZGI="
+      +    "dynamodb"
+           "install"
+         ]
+         "options": {
+      -    "service": "~�"
+      +    "service": "foo"
+         }
+       }
+      */
+      const expectedObject = { commands: ['dynamodb', 'install'], options: { service: 'foo' } };
+
+      expect(inputToBeProcessed).to.deep.equal(expectedObject);
+    });
+
     it('should return commands and options when both are given', () => {
       cli = new CLI(serverless, ['deploy', 'functions', '-f', 'function1']);
       const inputToBeProcessed = cli.processInput();