001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2016-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.examples;
037
038
039
040import java.io.BufferedReader;
041import java.io.FileInputStream;
042import java.io.FileReader;
043import java.io.FileOutputStream;
044import java.io.InputStream;
045import java.io.InputStreamReader;
046import java.io.OutputStream;
047import java.util.LinkedHashMap;
048
049import com.unboundid.ldap.sdk.ResultCode;
050import com.unboundid.ldap.sdk.Version;
051import com.unboundid.util.Base64;
052import com.unboundid.util.ByteStringBuffer;
053import com.unboundid.util.CommandLineTool;
054import com.unboundid.util.Debug;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.Nullable;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060import com.unboundid.util.args.ArgumentException;
061import com.unboundid.util.args.ArgumentParser;
062import com.unboundid.util.args.BooleanArgument;
063import com.unboundid.util.args.FileArgument;
064import com.unboundid.util.args.StringArgument;
065import com.unboundid.util.args.SubCommand;
066
067
068
069/**
070 * This class provides a tool that can be used to perform base64 encoding and
071 * decoding from the command line.  It provides two subcommands:  encode and
072 * decode.  Each of those subcommands offers the following arguments:
073 * <UL>
074 *   <LI>
075 *     "--data {data}" -- specifies the data to be encoded or decoded.
076 *   </LI>
077 *   <LI>
078 *     "--inputFile {data}" -- specifies the path to a file containing the data
079 *     to be encoded or decoded.
080 *   </LI>
081 *   <LI>
082 *     "--outputFile {data}" -- specifies the path to a file to which the
083 *     encoded or decoded data should be written.
084 *   </LI>
085 * </UL>
086 * The "--data" and "--inputFile" arguments are mutually exclusive, and if
087 * neither is provided, the data to encode will be read from standard input.
088 * If the "--outputFile" argument is not provided, then the result will be
089 * written to standard output.
090 */
091@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
092public final class Base64Tool
093       extends CommandLineTool
094{
095  /**
096   * The column at which to wrap long lines of output.
097   */
098  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
099
100
101
102  /**
103   * The name of the argument used to indicate whether to add an end-of-line
104   * marker to the end of the base64-encoded data.
105   */
106  @NotNull private static final String ARG_NAME_ADD_TRAILING_LINE_BREAK =
107       "addTrailingLineBreak";
108
109
110
111  /**
112   * The name of the argument used to specify the data to encode or decode.
113   */
114  @NotNull private static final String ARG_NAME_DATA = "data";
115
116
117
118  /**
119   * The name of the argument used to indicate whether to ignore any end-of-line
120   * marker that might be present at the end of the data to encode.
121   */
122  @NotNull private static final String ARG_NAME_IGNORE_TRAILING_LINE_BREAK =
123       "ignoreTrailingLineBreak";
124
125
126
127  /**
128   * The name of the argument used to specify the path to the input file with
129   * the data to encode or decode.
130   */
131  @NotNull private static final String ARG_NAME_INPUT_FILE = "inputFile";
132
133
134
135  /**
136   * The name of the argument used to specify the path to the output file into
137   * which to write the encoded or decoded data.
138   */
139  @NotNull private static final String ARG_NAME_OUTPUT_FILE = "outputFile";
140
141
142
143  /**
144   * The name of the argument used to indicate that the encoding and decoding
145   * should be performed using the base64url alphabet rather than the standard
146   * base64 alphabet.
147   */
148  @NotNull private static final String ARG_NAME_URL = "url";
149
150
151
152  /**
153   * The name of the subcommand used to decode data.
154   */
155  @NotNull private static final String SUBCOMMAND_NAME_DECODE = "decode";
156
157
158
159  /**
160   * The name of the subcommand used to encode data.
161   */
162  @NotNull private static final String SUBCOMMAND_NAME_ENCODE = "encode";
163
164
165
166  // The argument parser for this tool.
167  @Nullable private volatile ArgumentParser parser;
168
169  // The input stream to use as standard input.
170  @Nullable private final InputStream in;
171
172
173
174  /**
175   * Runs the tool with the provided set of arguments.
176   *
177   * @param  args  The command line arguments provided to this program.
178   */
179  public static void main(@NotNull final String... args)
180  {
181    final ResultCode resultCode = main(System.in, System.out, System.err, args);
182    if (resultCode != ResultCode.SUCCESS)
183    {
184      System.exit(resultCode.intValue());
185    }
186  }
187
188
189
190  /**
191   * Runs the tool with the provided information.
192   *
193   * @param  in    The input stream to use for standard input.  It may be
194   *               {@code null} if no standard input is needed.
195   * @param  out   The output stream to which standard out should be written.
196   *               It may be {@code null} if standard output should be
197   *               suppressed.
198   * @param  err   The output stream to which standard error should be written.
199   *               It may be {@code null} if standard error should be
200   *               suppressed.
201   * @param  args  The command line arguments provided to this program.
202   *
203   * @return  The result code obtained from running the tool.  A result code
204   *          other than {@link ResultCode#SUCCESS} will indicate that an error
205   *          occurred.
206   */
207  @NotNull()
208  public static ResultCode main(@Nullable final InputStream in,
209                                @Nullable final OutputStream out,
210                                @Nullable final OutputStream err,
211                                @NotNull final String... args)
212  {
213    final Base64Tool tool = new Base64Tool(in, out, err);
214    return tool.runTool(args);
215  }
216
217
218
219  /**
220   * Creates a new instance of this tool with the provided information.
221   * Standard input will not be available.
222   *
223   * @param  out  The output stream to which standard out should be written.
224   *              It may be {@code null} if standard output should be
225   *              suppressed.
226   * @param  err  The output stream to which standard error should be written.
227   *              It may be {@code null} if standard error should be suppressed.
228   */
229  public Base64Tool(@Nullable final OutputStream out,
230                    @Nullable final OutputStream err)
231  {
232    this(null, out, err);
233  }
234
235
236
237  /**
238   * Creates a new instance of this tool with the provided information.
239   *
240   * @param  in   The input stream to use for standard input.  It may be
241   *              {@code null} if no standard input is needed.
242   * @param  out  The output stream to which standard out should be written.
243   *              It may be {@code null} if standard output should be
244   *              suppressed.
245   * @param  err  The output stream to which standard error should be written.
246   *              It may be {@code null} if standard error should be suppressed.
247   */
248  public Base64Tool(@Nullable final InputStream in,
249                    @Nullable final OutputStream out,
250                    @Nullable final OutputStream err)
251  {
252    super(out, err);
253
254    this.in = in;
255
256    parser = null;
257  }
258
259
260
261  /**
262   * Retrieves the name of this tool.  It should be the name of the command used
263   * to invoke this tool.
264   *
265   * @return  The name for this tool.
266   */
267  @Override()
268  @NotNull()
269  public String getToolName()
270  {
271    return "base64";
272  }
273
274
275
276  /**
277   * Retrieves a human-readable description for this tool.
278   *
279   * @return  A human-readable description for this tool.
280   */
281  @Override()
282  @NotNull()
283  public String getToolDescription()
284  {
285    return "Encode raw data using the base64 algorithm or decode " +
286         "base64-encoded data back to its raw representation.";
287  }
288
289
290
291  /**
292   * Retrieves a version string for this tool, if available.
293   *
294   * @return  A version string for this tool, or {@code null} if none is
295   *          available.
296   */
297  @Override()
298  @NotNull()
299  public String getToolVersion()
300  {
301    return Version.NUMERIC_VERSION_STRING;
302  }
303
304
305
306  /**
307   * Indicates whether this tool should provide support for an interactive mode,
308   * in which the tool offers a mode in which the arguments can be provided in
309   * a text-driven menu rather than requiring them to be given on the command
310   * line.  If interactive mode is supported, it may be invoked using the
311   * "--interactive" argument.  Alternately, if interactive mode is supported
312   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
313   * interactive mode may be invoked by simply launching the tool without any
314   * arguments.
315   *
316   * @return  {@code true} if this tool supports interactive mode, or
317   *          {@code false} if not.
318   */
319  @Override()
320  public boolean supportsInteractiveMode()
321  {
322    return true;
323  }
324
325
326
327  /**
328   * Indicates whether this tool defaults to launching in interactive mode if
329   * the tool is invoked without any command-line arguments.  This will only be
330   * used if {@link #supportsInteractiveMode()} returns {@code true}.
331   *
332   * @return  {@code true} if this tool defaults to using interactive mode if
333   *          launched without any command-line arguments, or {@code false} if
334   *          not.
335   */
336  @Override()
337  public boolean defaultsToInteractiveMode()
338  {
339    return true;
340  }
341
342
343
344  /**
345   * Indicates whether this tool supports the use of a properties file for
346   * specifying default values for arguments that aren't specified on the
347   * command line.
348   *
349   * @return  {@code true} if this tool supports the use of a properties file
350   *          for specifying default values for arguments that aren't specified
351   *          on the command line, or {@code false} if not.
352   */
353  @Override()
354  public boolean supportsPropertiesFile()
355  {
356    return true;
357  }
358
359
360
361  /**
362   * Indicates whether this tool should provide arguments for redirecting output
363   * to a file.  If this method returns {@code true}, then the tool will offer
364   * an "--outputFile" argument that will specify the path to a file to which
365   * all standard output and standard error content will be written, and it will
366   * also offer a "--teeToStandardOut" argument that can only be used if the
367   * "--outputFile" argument is present and will cause all output to be written
368   * to both the specified output file and to standard output.
369   *
370   * @return  {@code true} if this tool should provide arguments for redirecting
371   *          output to a file, or {@code false} if not.
372   */
373  @Override()
374  protected boolean supportsOutputFile()
375  {
376    // This tool provides its own output file support.
377    return false;
378  }
379
380
381
382  /**
383   * Adds the command-line arguments supported for use with this tool to the
384   * provided argument parser.  The tool may need to retain references to the
385   * arguments (and/or the argument parser, if trailing arguments are allowed)
386   * to it in order to obtain their values for use in later processing.
387   *
388   * @param  parser  The argument parser to which the arguments are to be added.
389   *
390   * @throws  ArgumentException  If a problem occurs while adding any of the
391   *                             tool-specific arguments to the provided
392   *                             argument parser.
393   */
394  @Override()
395  public void addToolArguments(@NotNull final ArgumentParser parser)
396         throws ArgumentException
397  {
398    this.parser = parser;
399
400
401    // Create the subcommand for encoding data.
402    final ArgumentParser encodeParser =
403         new ArgumentParser("encode", "Base64-encodes raw data.");
404
405    final StringArgument encodeDataArgument = new StringArgument('d',
406         ARG_NAME_DATA, false, 1, "{data}",
407         "The raw data to be encoded.  If neither the --" + ARG_NAME_DATA +
408              " nor the --" + ARG_NAME_INPUT_FILE + " argument is provided, " +
409              "then the data will be read from standard input.");
410    encodeDataArgument.addLongIdentifier("rawData", true);
411    encodeDataArgument.addLongIdentifier("raw-data", true);
412    encodeParser.addArgument(encodeDataArgument);
413
414    final FileArgument encodeDataFileArgument = new FileArgument('f',
415         ARG_NAME_INPUT_FILE, false, 1, null,
416         "The path to a file containing the raw data to be encoded.  If " +
417              "neither the --" + ARG_NAME_DATA + " nor the --" +
418              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
419              "will be read from standard input.",
420         true, true, true, false);
421    encodeDataFileArgument.addLongIdentifier("rawDataFile", true);
422    encodeDataFileArgument.addLongIdentifier("input-file", true);
423    encodeDataFileArgument.addLongIdentifier("raw-data-file", true);
424    encodeParser.addArgument(encodeDataFileArgument);
425
426    final FileArgument encodeOutputFileArgument = new FileArgument('o',
427         ARG_NAME_OUTPUT_FILE, false, 1, null,
428         "The path to a file to which the encoded data should be written.  " +
429              "If this is not provided, the encoded data will be written to " +
430              "standard output.",
431         false, true, true, false);
432    encodeOutputFileArgument.addLongIdentifier("toEncodedFile", true);
433    encodeOutputFileArgument.addLongIdentifier("output-file", true);
434    encodeOutputFileArgument.addLongIdentifier("to-encoded-file", true);
435    encodeParser.addArgument(encodeOutputFileArgument);
436
437    final BooleanArgument encodeURLArgument = new BooleanArgument(null,
438         ARG_NAME_URL,
439         "Encode the data with the base64url mechanism rather than the " +
440              "standard base64 mechanism.");
441    encodeParser.addArgument(encodeURLArgument);
442
443    final BooleanArgument encodeIgnoreTrailingEOLArgument = new BooleanArgument(
444         null, ARG_NAME_IGNORE_TRAILING_LINE_BREAK,
445         "Ignore any end-of-line marker that may be present at the end of " +
446              "the data to encode.");
447    encodeIgnoreTrailingEOLArgument.addLongIdentifier(
448         "ignore-trailing-line-break", true);
449    encodeParser.addArgument(encodeIgnoreTrailingEOLArgument);
450
451    encodeParser.addExclusiveArgumentSet(encodeDataArgument,
452         encodeDataFileArgument);
453
454    final LinkedHashMap<String[],String> encodeExamples =
455         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
456    encodeExamples.put(
457         new String[]
458         {
459           "encode",
460           "--data", "Hello"
461         },
462         "Base64-encodes the string 'Hello' and writes the result to " +
463              "standard output.");
464    encodeExamples.put(
465         new String[]
466         {
467           "encode",
468           "--inputFile", "raw-data.txt",
469           "--outputFile", "encoded-data.txt",
470         },
471         "Base64-encodes the data contained in the 'raw-data.txt' file and " +
472              "writes the result to the 'encoded-data.txt' file.");
473    encodeExamples.put(
474         new String[]
475         {
476           "encode"
477         },
478         "Base64-encodes data read from standard input and writes the result " +
479              "to standard output.");
480
481    final SubCommand encodeSubCommand = new SubCommand(SUBCOMMAND_NAME_ENCODE,
482         "Base64-encodes raw data.", encodeParser, encodeExamples);
483    parser.addSubCommand(encodeSubCommand);
484
485
486    // Create the subcommand for decoding data.
487    final ArgumentParser decodeParser =
488         new ArgumentParser("decode", "Decodes base64-encoded data.");
489
490    final StringArgument decodeDataArgument = new StringArgument('d',
491         ARG_NAME_DATA, false, 1, "{data}",
492         "The base64-encoded data to be decoded.  If neither the --" +
493              ARG_NAME_DATA + " nor the --" + ARG_NAME_INPUT_FILE +
494              " argument is provided, then the data will be read from " +
495              "standard input.");
496    decodeDataArgument.addLongIdentifier("encodedData", true);
497    decodeDataArgument.addLongIdentifier("encoded-data", true);
498    decodeParser.addArgument(decodeDataArgument);
499
500    final FileArgument decodeDataFileArgument = new FileArgument('f',
501         ARG_NAME_INPUT_FILE, false, 1, null,
502         "The path to a file containing the base64-encoded data to be " +
503              "decoded.  If neither the --" + ARG_NAME_DATA + " nor the --" +
504              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
505              "will be read from standard input.",
506         true, true, true, false);
507    decodeDataFileArgument.addLongIdentifier("encodedDataFile", true);
508    decodeDataFileArgument.addLongIdentifier("input-file", true);
509    decodeDataFileArgument.addLongIdentifier("encoded-data-file", true);
510    decodeParser.addArgument(decodeDataFileArgument);
511
512    final FileArgument decodeOutputFileArgument = new FileArgument('o',
513         ARG_NAME_OUTPUT_FILE, false, 1, null,
514         "The path to a file to which the decoded data should be written.  " +
515              "If this is not provided, the decoded data will be written to " +
516              "standard output.",
517         false, true, true, false);
518    decodeOutputFileArgument.addLongIdentifier("toRawFile", true);
519    decodeOutputFileArgument.addLongIdentifier("output-file", true);
520    decodeOutputFileArgument.addLongIdentifier("to-raw-file", true);
521    decodeParser.addArgument(decodeOutputFileArgument);
522
523    final BooleanArgument decodeURLArgument = new BooleanArgument(null,
524         ARG_NAME_URL,
525         "Decode the data with the base64url mechanism rather than the " +
526              "standard base64 mechanism.");
527    decodeParser.addArgument(decodeURLArgument);
528
529    final BooleanArgument decodeAddTrailingLineBreak = new BooleanArgument(
530         null, ARG_NAME_ADD_TRAILING_LINE_BREAK,
531         "Add a line break to the end of the decoded data.");
532    decodeAddTrailingLineBreak.addLongIdentifier("add-trailing-line-break",
533         true);
534    decodeParser.addArgument(decodeAddTrailingLineBreak);
535
536    decodeParser.addExclusiveArgumentSet(decodeDataArgument,
537         decodeDataFileArgument);
538
539    final LinkedHashMap<String[],String> decodeExamples =
540         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
541    decodeExamples.put(
542         new String[]
543         {
544           "decode",
545           "--data", "SGVsbG8="
546         },
547         "Base64-decodes the string 'SGVsbG8=' and writes the result to " +
548              "standard output.");
549    decodeExamples.put(
550         new String[]
551         {
552           "decode",
553           "--inputFile", "encoded-data.txt",
554           "--outputFile", "decoded-data.txt",
555         },
556         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
557              "and writes the result to the 'raw-data.txt' file.");
558    decodeExamples.put(
559         new String[]
560         {
561           "decode"
562         },
563         "Base64-decodes data read from standard input and writes the result " +
564              "to standard output.");
565
566    final SubCommand decodeSubCommand = new SubCommand(SUBCOMMAND_NAME_DECODE,
567         "Decodes base64-encoded data.", decodeParser, decodeExamples);
568    parser.addSubCommand(decodeSubCommand);
569  }
570
571
572
573  /**
574   * Performs the core set of processing for this tool.
575   *
576   * @return  A result code that indicates whether the processing completed
577   *          successfully.
578   */
579  @Override()
580  @NotNull()
581  public ResultCode doToolProcessing()
582  {
583    // Get the subcommand selected by the user.
584    final SubCommand subCommand = parser.getSelectedSubCommand();
585    if (subCommand == null)
586    {
587      // This should never happen.
588      wrapErr(0, WRAP_COLUMN, "No subcommand was selected.");
589      return ResultCode.PARAM_ERROR;
590    }
591
592
593    // Take the appropriate action based on the selected subcommand.
594    if (subCommand.hasName(SUBCOMMAND_NAME_ENCODE))
595    {
596      return doEncode(subCommand.getArgumentParser());
597    }
598    else
599    {
600      return doDecode(subCommand.getArgumentParser());
601    }
602  }
603
604
605
606  /**
607   * Performs the necessary work for base64 encoding.
608   *
609   * @param  p  The argument parser for the encode subcommand.
610   *
611   * @return  A result code that indicates whether the processing completed
612   *          successfully.
613   */
614  @NotNull()
615  private ResultCode doEncode(@NotNull final ArgumentParser p)
616  {
617    // Get the data to encode.
618    final ByteStringBuffer rawDataBuffer = new ByteStringBuffer();
619    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
620    if ((dataArg != null) && dataArg.isPresent())
621    {
622      rawDataBuffer.append(dataArg.getValue());
623    }
624    else
625    {
626      try
627      {
628        final InputStream inputStream;
629        final FileArgument inputFileArg =
630             p.getFileArgument(ARG_NAME_INPUT_FILE);
631        if ((inputFileArg != null) && inputFileArg.isPresent())
632        {
633          inputStream = new FileInputStream(inputFileArg.getValue());
634        }
635        else
636        {
637          inputStream = in;
638        }
639
640        final byte[] buffer = new byte[8192];
641        while (true)
642        {
643          final int bytesRead = inputStream.read(buffer);
644          if (bytesRead <= 0)
645          {
646            break;
647          }
648
649          rawDataBuffer.append(buffer, 0, bytesRead);
650        }
651
652        inputStream.close();
653      }
654      catch (final Exception e)
655      {
656        Debug.debugException(e);
657        wrapErr(0, WRAP_COLUMN,
658             "An error occurred while attempting to read the data to encode:  ",
659             StaticUtils.getExceptionMessage(e));
660        return ResultCode.LOCAL_ERROR;
661      }
662    }
663
664
665    // If we should ignore any trailing end-of-line markers, then do that now.
666    final BooleanArgument ignoreEOLArg =
667         p.getBooleanArgument(ARG_NAME_IGNORE_TRAILING_LINE_BREAK);
668    if ((ignoreEOLArg != null) && ignoreEOLArg.isPresent())
669    {
670stripEOLLoop:
671      while (rawDataBuffer.length() > 0)
672      {
673        switch (rawDataBuffer.getBackingArray()[rawDataBuffer.length() - 1])
674        {
675          case '\n':
676          case '\r':
677            rawDataBuffer.delete(rawDataBuffer.length() - 1, 1);
678            break;
679          default:
680            break stripEOLLoop;
681        }
682      }
683    }
684
685
686    // Base64-encode the data.
687    final byte[] rawDataArray = rawDataBuffer.toByteArray();
688    final ByteStringBuffer encodedDataBuffer =
689         new ByteStringBuffer(4 * rawDataBuffer.length() / 3 + 3);
690    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
691    if ((urlArg != null) && urlArg.isPresent())
692    {
693      Base64.urlEncode(rawDataArray, 0, rawDataArray.length, encodedDataBuffer,
694           false);
695    }
696    else
697    {
698      Base64.encode(rawDataArray, encodedDataBuffer);
699    }
700
701
702    // Write the encoded data.
703    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
704    if ((outputFileArg != null) && outputFileArg.isPresent())
705    {
706      try
707      {
708        final FileOutputStream outputStream =
709             new FileOutputStream(outputFileArg.getValue(), false);
710        encodedDataBuffer.write(outputStream);
711        outputStream.write(StaticUtils.EOL_BYTES);
712        outputStream.flush();
713        outputStream.close();
714      }
715      catch (final Exception e)
716      {
717        Debug.debugException(e);
718        wrapErr(0, WRAP_COLUMN,
719             "An error occurred while attempting to write the base64-encoded " +
720                  "data to output file ",
721             outputFileArg.getValue().getAbsolutePath(), ":  ",
722             StaticUtils.getExceptionMessage(e));
723        err("Base64-encoded data:");
724        err(encodedDataBuffer.toString());
725        return ResultCode.LOCAL_ERROR;
726      }
727    }
728    else
729    {
730      out(encodedDataBuffer.toString());
731    }
732
733
734    return ResultCode.SUCCESS;
735  }
736
737
738
739  /**
740   * Performs the necessary work for base64 decoding.
741   *
742   * @param  p  The argument parser for the decode subcommand.
743   *
744   * @return  A result code that indicates whether the processing completed
745   *          successfully.
746   */
747  @NotNull()
748  private ResultCode doDecode(@NotNull final ArgumentParser p)
749  {
750    // Get the data to decode.  We'll always ignore the following:
751    // - Line breaks
752    // - Blank lines
753    // - Lines that start with an octothorpe (#)
754    //
755    // Unless the --url argument was provided, then we'll also ignore lines that
756    // start with a dash (like those used as start and end markers in a
757    // PEM-encoded certificate).  Since dashes are part of the base64url
758    // alphabet, we can't ignore dashes if the --url argument was provided.
759    final ByteStringBuffer encodedDataBuffer = new ByteStringBuffer();
760    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
761    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
762    if ((dataArg != null) && dataArg.isPresent())
763    {
764      encodedDataBuffer.append(dataArg.getValue());
765    }
766    else
767    {
768      try
769      {
770        final BufferedReader reader;
771        final FileArgument inputFileArg =
772             p.getFileArgument(ARG_NAME_INPUT_FILE);
773        if ((inputFileArg != null) && inputFileArg.isPresent())
774        {
775          reader = new BufferedReader(new FileReader(inputFileArg.getValue()));
776        }
777        else
778        {
779          reader = new BufferedReader(new InputStreamReader(in));
780        }
781
782        while (true)
783        {
784          final String line = reader.readLine();
785          if (line == null)
786          {
787            break;
788          }
789
790          if ((line.length() == 0) || line.startsWith("#"))
791          {
792            continue;
793          }
794
795          if (line.startsWith("-") &&
796              ((urlArg == null) || (! urlArg.isPresent())))
797          {
798            continue;
799          }
800
801          encodedDataBuffer.append(line);
802        }
803
804        reader.close();
805      }
806      catch (final Exception e)
807      {
808        Debug.debugException(e);
809        wrapErr(0, WRAP_COLUMN,
810             "An error occurred while attempting to read the data to decode:  ",
811             StaticUtils.getExceptionMessage(e));
812        return ResultCode.LOCAL_ERROR;
813      }
814    }
815
816
817    // Base64-decode the data.
818    final ByteStringBuffer rawDataBuffer = new
819         ByteStringBuffer(encodedDataBuffer.length());
820    if ((urlArg != null) && urlArg.isPresent())
821    {
822      try
823      {
824        rawDataBuffer.append(Base64.urlDecode(encodedDataBuffer.toString()));
825      }
826      catch (final Exception e)
827      {
828        Debug.debugException(e);
829        wrapErr(0, WRAP_COLUMN,
830             "An error occurred while attempting to base64url-decode the " +
831                  "provided data:  " + StaticUtils.getExceptionMessage(e));
832        return ResultCode.LOCAL_ERROR;
833      }
834    }
835    else
836    {
837      try
838      {
839        rawDataBuffer.append(Base64.decode(encodedDataBuffer.toString()));
840      }
841      catch (final Exception e)
842      {
843        Debug.debugException(e);
844        wrapErr(0, WRAP_COLUMN,
845             "An error occurred while attempting to base64-decode the " +
846                  "provided data:  " + StaticUtils.getExceptionMessage(e));
847        return ResultCode.LOCAL_ERROR;
848      }
849    }
850
851
852    // If we should add a newline, then do that now.
853    final BooleanArgument addEOLArg =
854         p.getBooleanArgument(ARG_NAME_ADD_TRAILING_LINE_BREAK);
855    if ((addEOLArg != null) && addEOLArg.isPresent())
856    {
857      rawDataBuffer.append(StaticUtils.EOL_BYTES);
858    }
859
860
861    // Write the decoded data.
862    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
863    if ((outputFileArg != null) && outputFileArg.isPresent())
864    {
865      try
866      {
867        final FileOutputStream outputStream =
868             new FileOutputStream(outputFileArg.getValue(), false);
869        rawDataBuffer.write(outputStream);
870        outputStream.flush();
871        outputStream.close();
872      }
873      catch (final Exception e)
874      {
875        Debug.debugException(e);
876        wrapErr(0, WRAP_COLUMN,
877             "An error occurred while attempting to write the base64-decoded " +
878                  "data to output file ",
879             outputFileArg.getValue().getAbsolutePath(), ":  ",
880             StaticUtils.getExceptionMessage(e));
881        err("Base64-decoded data:");
882        err(encodedDataBuffer.toString());
883        return ResultCode.LOCAL_ERROR;
884      }
885    }
886    else
887    {
888      final byte[] rawDataArray = rawDataBuffer.toByteArray();
889      getOut().write(rawDataArray, 0, rawDataArray.length);
890      getOut().flush();
891    }
892
893
894    return ResultCode.SUCCESS;
895  }
896
897
898
899  /**
900   * Retrieves a set of information that may be used to generate example usage
901   * information.  Each element in the returned map should consist of a map
902   * between an example set of arguments and a string that describes the
903   * behavior of the tool when invoked with that set of arguments.
904   *
905   * @return  A set of information that may be used to generate example usage
906   *          information.  It may be {@code null} or empty if no example usage
907   *          information is available.
908   */
909  @Override()
910  @NotNull()
911  public LinkedHashMap<String[],String> getExampleUsages()
912  {
913    final LinkedHashMap<String[],String> examples =
914         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
915
916    examples.put(
917         new String[]
918         {
919           "encode",
920           "--data", "Hello"
921         },
922         "Base64-encodes the string 'Hello' and writes the result to " +
923              "standard output.");
924
925    examples.put(
926         new String[]
927         {
928           "decode",
929           "--inputFile", "encoded-data.txt",
930           "--outputFile", "decoded-data.txt",
931         },
932         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
933              "and writes the result to the 'raw-data.txt' file.");
934
935    return examples;
936  }
937}