RLPSubCommand.java

/*
 * Copyright ConsenSys AG.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.cli.subcommands.rlp;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;

import org.hyperledger.besu.cli.BesuCommand;
import org.hyperledger.besu.cli.DefaultCommandValues;
import org.hyperledger.besu.cli.subcommands.rlp.RLPSubCommand.DecodeSubCommand;
import org.hyperledger.besu.cli.subcommands.rlp.RLPSubCommand.EncodeSubCommand;
import org.hyperledger.besu.cli.util.VersionProvider;
import org.hyperledger.besu.consensus.common.bft.BftExtraData;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.NoSuchElementException;
import java.util.Scanner;

import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import org.apache.tuweni.bytes.Bytes;
import picocli.CommandLine.Command;
import picocli.CommandLine.ExecutionException;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.ParentCommand;
import picocli.CommandLine.Spec;

/** The RLP sub command. */
@Command(
    name = RLPSubCommand.COMMAND_NAME,
    description = "This command provides RLP data related actions.",
    mixinStandardHelpOptions = true,
    versionProvider = VersionProvider.class,
    subcommands = {EncodeSubCommand.class, DecodeSubCommand.class})
public class RLPSubCommand implements Runnable {

  /** The constant COMMAND_NAME. */
  public static final String COMMAND_NAME = "rlp";

  private final PrintWriter out;
  private final InputStream in;

  @SuppressWarnings("unused")
  @ParentCommand
  private BesuCommand parentCommand;

  @SuppressWarnings("unused")
  @Spec
  private CommandSpec spec;

  /**
   * Instantiates a new Rlp sub command.
   *
   * @param out the PrintWriter where the output of subcommand will be reported
   * @param in the InputStream which will be used to read the input for this subcommand.
   */
  public RLPSubCommand(final PrintWriter out, final InputStream in) {
    this.out = out;
    this.in = in;
  }

  @Override
  public void run() {
    spec.commandLine().usage(out);
  }

  /**
   * RLP encode sub-command
   *
   * <p>Encode a JSON data into an RLP hex string.
   */
  @Command(
      name = "encode",
      description = "This command encodes a JSON typed data into an RLP hex string.",
      mixinStandardHelpOptions = true,
      versionProvider = VersionProvider.class)
  static class EncodeSubCommand implements Runnable {

    @SuppressWarnings("unused")
    @ParentCommand
    private RLPSubCommand parentCommand; // Picocli injects reference to parent command

    @SuppressWarnings("unused")
    @Spec
    private CommandSpec spec;

    @Option(
        names = "--type",
        description =
            "Type of the RLP data to encode, possible values are ${COMPLETION-CANDIDATES}. (default: ${DEFAULT-VALUE})",
        arity = "1..1")
    private final RLPType type = RLPType.IBFT_EXTRA_DATA;

    @Option(
        names = "--from",
        paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
        description = "File containing JSON object to encode",
        arity = "1..1")
    private final File jsonSourceFile = null;

    @Option(
        names = "--to",
        paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
        description = "File to write encoded RLP string to.",
        arity = "1..1")
    private final File rlpTargetFile = null;

    @Override
    public void run() {
      checkNotNull(parentCommand);
      readInput();
    }

    /**
     * Reads the stdin or from a file if one is specified by {@link #jsonSourceFile} then goes to
     * {@link #encode(String)} this data
     */
    private void readInput() {
      // if we have an output file defined, print to it
      // otherwise print to defined output, usually standard output.
      StringBuilder jsonData = new StringBuilder();

      if (jsonSourceFile != null) {
        try {
          BufferedReader reader = Files.newBufferedReader(jsonSourceFile.toPath(), UTF_8);

          String line;
          while ((line = reader.readLine()) != null) jsonData.append(line);
        } catch (IOException e) {
          throw new ExecutionException(spec.commandLine(), "Unable to read JSON file.");
        }
      } else {
        // get JSON data from standard input
        try (Scanner scanner = new Scanner(parentCommand.in, UTF_8.name())) {
          while (scanner.hasNextLine()) {
            jsonData.append(String.join("", scanner.nextLine().split("\\s")));
          }
        }
      }

      // next step is to encode the value
      encode(jsonData.toString());
    }

    /**
     * Encodes the JSON input into an RLP data based on the {@link #type} then goes to {@link
     * #writeOutput(Bytes)} this data to file or stdout
     *
     * @param jsonInput the JSON string data to encode
     */
    private void encode(final String jsonInput) {
      if (jsonInput == null || jsonInput.isEmpty()) {
        throw new ParameterException(
            spec.commandLine(), "An error occurred while trying to read the JSON data.");
      } else {
        try {
          // encode and write the value
          writeOutput(type.getAdapter().encode(jsonInput));
        } catch (MismatchedInputException e) {
          throw new ParameterException(
              spec.commandLine(),
              "Unable to map the JSON data with selected type. Please check JSON input format. "
                  + e);
        } catch (IOException e) {
          throw new ParameterException(
              spec.commandLine(),
              "Unable to load the JSON data. Please check JSON input format. " + e);
        }
      }
    }

    /**
     * write the encoded result to stdout or a file if the option is specified
     *
     * @param rlpEncodedOutput the RLP output to write to file or stdout
     */
    private void writeOutput(final Bytes rlpEncodedOutput) {
      if (rlpTargetFile != null) {
        final Path targetPath = rlpTargetFile.toPath();

        try (final BufferedWriter fileWriter = Files.newBufferedWriter(targetPath, UTF_8)) {
          fileWriter.write(rlpEncodedOutput.toString());
        } catch (final IOException e) {
          throw new ParameterException(
              spec.commandLine(),
              "An error occurred while trying to write the RLP string. " + e.getMessage());
        }
      } else {
        parentCommand.out.println(rlpEncodedOutput);
      }
    }
  }

  /**
   * RLP decode sub-command
   *
   * <p>Decode a RLP hex string into a validator list.
   */
  @Command(
      name = "decode",
      description = "This command decodes a JSON typed RLP hex string into validator list.",
      mixinStandardHelpOptions = true,
      versionProvider = VersionProvider.class)
  static class DecodeSubCommand implements Runnable {

    @SuppressWarnings("unused")
    @ParentCommand
    private RLPSubCommand parentCommand; // Picocli injects reference to parent command

    @SuppressWarnings("unused")
    @Spec
    private CommandSpec spec;

    @Option(
        names = "--type",
        description =
            "Type of the RLP data to Decode, possible values are ${COMPLETION-CANDIDATES}. (default: ${DEFAULT-VALUE})",
        arity = "1..1")
    private final RLPType type = RLPType.IBFT_EXTRA_DATA;

    @Option(
        names = "--from",
        paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
        description = "File containing JSON object to decode",
        arity = "1..1")
    private final File jsonSourceFile = null;

    @Option(
        names = "--to",
        paramLabel = DefaultCommandValues.MANDATORY_FILE_FORMAT_HELP,
        description = "File to write decoded RLP string to.",
        arity = "1..1")
    private final File rlpTargetFile = null;

    @Override
    public void run() {
      checkNotNull(parentCommand);
      readInput();
    }

    /**
     * Reads the stdin or from a file if one is specified by {@link #jsonSourceFile} then goes to
     * {@link #decode(String)} this data
     */
    private void readInput() {
      // if we have an output file defined, print to it
      // otherwise print to defined output, usually standard output.
      final String inputData;

      if (jsonSourceFile != null) {
        try {
          BufferedReader reader = Files.newBufferedReader(jsonSourceFile.toPath(), UTF_8);

          // Read only the first line if there are many lines
          inputData = reader.readLine();
        } catch (IOException e) {
          throw new ExecutionException(spec.commandLine(), "Unable to read input file.");
        }
      } else {
        // get data from standard input
        try (Scanner scanner = new Scanner(parentCommand.in, UTF_8.name())) {
          inputData = scanner.nextLine();
        } catch (NoSuchElementException e) {
          throw new ParameterException(spec.commandLine(), "Unable to read input data." + e);
        }
      }

      decode(inputData);
    }

    /**
     * Decodes the string input into an validator data based on the {@link #type} then goes to
     * {@link #writeOutput(BftExtraData)} this data to file or stdout
     *
     * @param inputData the string data to decode
     */
    private void decode(final String inputData) {
      if (inputData == null || inputData.isEmpty()) {
        throw new ParameterException(
            spec.commandLine(), "An error occurred while trying to read the input data.");
      } else {
        try {
          // decode and write the value
          writeOutput(type.getAdapter().decode(inputData));
        } catch (MismatchedInputException e) {
          throw new ParameterException(
              spec.commandLine(),
              "Unable to map the input data with selected type. Please check input format. " + e);
        } catch (IOException e) {
          throw new ParameterException(
              spec.commandLine(), "Unable to load the input data. Please check input format. " + e);
        }
      }
    }

    /**
     * write the decoded result to stdout or a file if the option is specified
     *
     * @param bftExtraDataOutput the BFT extra data output to write to file or stdout
     */
    private void writeOutput(final BftExtraData bftExtraDataOutput) {
      if (rlpTargetFile != null) {
        final Path targetPath = rlpTargetFile.toPath();

        try (final BufferedWriter fileWriter = Files.newBufferedWriter(targetPath, UTF_8)) {
          fileWriter.write(bftExtraDataOutput.getValidators().toString());
        } catch (final IOException e) {
          throw new ParameterException(
              spec.commandLine(),
              "An error occurred while trying to write the validator list. " + e.getMessage());
        }
      } else {
        parentCommand.out.println(bftExtraDataOutput.getValidators().toString());
      }
    }
  }
}