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   * Indicates whether this tool supports the ability to generate a debug log
384   * file.  If this method returns {@code true}, then the tool will expose
385   * additional arguments that can control debug logging.
386   *
387   * @return  {@code true} if this tool supports the ability to generate a debug
388   *          log file, or {@code false} if not.
389   */
390  @Override()
391  protected boolean supportsDebugLogging()
392  {
393    return true;
394  }
395
396
397
398  /**
399   * Adds the command-line arguments supported for use with this tool to the
400   * provided argument parser.  The tool may need to retain references to the
401   * arguments (and/or the argument parser, if trailing arguments are allowed)
402   * to it in order to obtain their values for use in later processing.
403   *
404   * @param  parser  The argument parser to which the arguments are to be added.
405   *
406   * @throws  ArgumentException  If a problem occurs while adding any of the
407   *                             tool-specific arguments to the provided
408   *                             argument parser.
409   */
410  @Override()
411  public void addToolArguments(@NotNull final ArgumentParser parser)
412         throws ArgumentException
413  {
414    this.parser = parser;
415
416
417    // Create the subcommand for encoding data.
418    final ArgumentParser encodeParser =
419         new ArgumentParser("encode", "Base64-encodes raw data.");
420
421    final StringArgument encodeDataArgument = new StringArgument('d',
422         ARG_NAME_DATA, false, 1, "{data}",
423         "The raw data to be encoded.  If neither the --" + ARG_NAME_DATA +
424              " nor the --" + ARG_NAME_INPUT_FILE + " argument is provided, " +
425              "then the data will be read from standard input.");
426    encodeDataArgument.addLongIdentifier("rawData", true);
427    encodeDataArgument.addLongIdentifier("raw-data", true);
428    encodeParser.addArgument(encodeDataArgument);
429
430    final FileArgument encodeDataFileArgument = new FileArgument('f',
431         ARG_NAME_INPUT_FILE, false, 1, null,
432         "The path to a file containing the raw data to be encoded.  If " +
433              "neither the --" + ARG_NAME_DATA + " nor the --" +
434              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
435              "will be read from standard input.",
436         true, true, true, false);
437    encodeDataFileArgument.addLongIdentifier("rawDataFile", true);
438    encodeDataFileArgument.addLongIdentifier("input-file", true);
439    encodeDataFileArgument.addLongIdentifier("raw-data-file", true);
440    encodeParser.addArgument(encodeDataFileArgument);
441
442    final FileArgument encodeOutputFileArgument = new FileArgument('o',
443         ARG_NAME_OUTPUT_FILE, false, 1, null,
444         "The path to a file to which the encoded data should be written.  " +
445              "If this is not provided, the encoded data will be written to " +
446              "standard output.",
447         false, true, true, false);
448    encodeOutputFileArgument.addLongIdentifier("toEncodedFile", true);
449    encodeOutputFileArgument.addLongIdentifier("output-file", true);
450    encodeOutputFileArgument.addLongIdentifier("to-encoded-file", true);
451    encodeParser.addArgument(encodeOutputFileArgument);
452
453    final BooleanArgument encodeURLArgument = new BooleanArgument(null,
454         ARG_NAME_URL,
455         "Encode the data with the base64url mechanism rather than the " +
456              "standard base64 mechanism.");
457    encodeParser.addArgument(encodeURLArgument);
458
459    final BooleanArgument encodeIgnoreTrailingEOLArgument = new BooleanArgument(
460         null, ARG_NAME_IGNORE_TRAILING_LINE_BREAK,
461         "Ignore any end-of-line marker that may be present at the end of " +
462              "the data to encode.");
463    encodeIgnoreTrailingEOLArgument.addLongIdentifier(
464         "ignore-trailing-line-break", true);
465    encodeParser.addArgument(encodeIgnoreTrailingEOLArgument);
466
467    encodeParser.addExclusiveArgumentSet(encodeDataArgument,
468         encodeDataFileArgument);
469
470    final LinkedHashMap<String[],String> encodeExamples =
471         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
472    encodeExamples.put(
473         new String[]
474         {
475           "encode",
476           "--data", "Hello"
477         },
478         "Base64-encodes the string 'Hello' and writes the result to " +
479              "standard output.");
480    encodeExamples.put(
481         new String[]
482         {
483           "encode",
484           "--inputFile", "raw-data.txt",
485           "--outputFile", "encoded-data.txt",
486         },
487         "Base64-encodes the data contained in the 'raw-data.txt' file and " +
488              "writes the result to the 'encoded-data.txt' file.");
489    encodeExamples.put(
490         new String[]
491         {
492           "encode"
493         },
494         "Base64-encodes data read from standard input and writes the result " +
495              "to standard output.");
496
497    final SubCommand encodeSubCommand = new SubCommand(SUBCOMMAND_NAME_ENCODE,
498         "Base64-encodes raw data.", encodeParser, encodeExamples);
499    parser.addSubCommand(encodeSubCommand);
500
501
502    // Create the subcommand for decoding data.
503    final ArgumentParser decodeParser =
504         new ArgumentParser("decode", "Decodes base64-encoded data.");
505
506    final StringArgument decodeDataArgument = new StringArgument('d',
507         ARG_NAME_DATA, false, 1, "{data}",
508         "The base64-encoded data to be decoded.  If neither the --" +
509              ARG_NAME_DATA + " nor the --" + ARG_NAME_INPUT_FILE +
510              " argument is provided, then the data will be read from " +
511              "standard input.");
512    decodeDataArgument.addLongIdentifier("encodedData", true);
513    decodeDataArgument.addLongIdentifier("encoded-data", true);
514    decodeParser.addArgument(decodeDataArgument);
515
516    final FileArgument decodeDataFileArgument = new FileArgument('f',
517         ARG_NAME_INPUT_FILE, false, 1, null,
518         "The path to a file containing the base64-encoded data to be " +
519              "decoded.  If neither the --" + ARG_NAME_DATA + " nor the --" +
520              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
521              "will be read from standard input.",
522         true, true, true, false);
523    decodeDataFileArgument.addLongIdentifier("encodedDataFile", true);
524    decodeDataFileArgument.addLongIdentifier("input-file", true);
525    decodeDataFileArgument.addLongIdentifier("encoded-data-file", true);
526    decodeParser.addArgument(decodeDataFileArgument);
527
528    final FileArgument decodeOutputFileArgument = new FileArgument('o',
529         ARG_NAME_OUTPUT_FILE, false, 1, null,
530         "The path to a file to which the decoded data should be written.  " +
531              "If this is not provided, the decoded data will be written to " +
532              "standard output.",
533         false, true, true, false);
534    decodeOutputFileArgument.addLongIdentifier("toRawFile", true);
535    decodeOutputFileArgument.addLongIdentifier("output-file", true);
536    decodeOutputFileArgument.addLongIdentifier("to-raw-file", true);
537    decodeParser.addArgument(decodeOutputFileArgument);
538
539    final BooleanArgument decodeURLArgument = new BooleanArgument(null,
540         ARG_NAME_URL,
541         "Decode the data with the base64url mechanism rather than the " +
542              "standard base64 mechanism.");
543    decodeParser.addArgument(decodeURLArgument);
544
545    final BooleanArgument decodeAddTrailingLineBreak = new BooleanArgument(
546         null, ARG_NAME_ADD_TRAILING_LINE_BREAK,
547         "Add a line break to the end of the decoded data.");
548    decodeAddTrailingLineBreak.addLongIdentifier("add-trailing-line-break",
549         true);
550    decodeParser.addArgument(decodeAddTrailingLineBreak);
551
552    decodeParser.addExclusiveArgumentSet(decodeDataArgument,
553         decodeDataFileArgument);
554
555    final LinkedHashMap<String[],String> decodeExamples =
556         new LinkedHashMap<>(StaticUtils.computeMapCapacity(3));
557    decodeExamples.put(
558         new String[]
559         {
560           "decode",
561           "--data", "SGVsbG8="
562         },
563         "Base64-decodes the string 'SGVsbG8=' and writes the result to " +
564              "standard output.");
565    decodeExamples.put(
566         new String[]
567         {
568           "decode",
569           "--inputFile", "encoded-data.txt",
570           "--outputFile", "decoded-data.txt",
571         },
572         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
573              "and writes the result to the 'raw-data.txt' file.");
574    decodeExamples.put(
575         new String[]
576         {
577           "decode"
578         },
579         "Base64-decodes data read from standard input and writes the result " +
580              "to standard output.");
581
582    final SubCommand decodeSubCommand = new SubCommand(SUBCOMMAND_NAME_DECODE,
583         "Decodes base64-encoded data.", decodeParser, decodeExamples);
584    parser.addSubCommand(decodeSubCommand);
585  }
586
587
588
589  /**
590   * Performs the core set of processing for this tool.
591   *
592   * @return  A result code that indicates whether the processing completed
593   *          successfully.
594   */
595  @Override()
596  @NotNull()
597  public ResultCode doToolProcessing()
598  {
599    // Get the subcommand selected by the user.
600    final SubCommand subCommand = parser.getSelectedSubCommand();
601    if (subCommand == null)
602    {
603      // This should never happen.
604      wrapErr(0, WRAP_COLUMN, "No subcommand was selected.");
605      return ResultCode.PARAM_ERROR;
606    }
607
608
609    // Take the appropriate action based on the selected subcommand.
610    if (subCommand.hasName(SUBCOMMAND_NAME_ENCODE))
611    {
612      return doEncode(subCommand.getArgumentParser());
613    }
614    else
615    {
616      return doDecode(subCommand.getArgumentParser());
617    }
618  }
619
620
621
622  /**
623   * Performs the necessary work for base64 encoding.
624   *
625   * @param  p  The argument parser for the encode subcommand.
626   *
627   * @return  A result code that indicates whether the processing completed
628   *          successfully.
629   */
630  @NotNull()
631  private ResultCode doEncode(@NotNull final ArgumentParser p)
632  {
633    // Get the data to encode.
634    final ByteStringBuffer rawDataBuffer = new ByteStringBuffer();
635    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
636    if ((dataArg != null) && dataArg.isPresent())
637    {
638      rawDataBuffer.append(dataArg.getValue());
639    }
640    else
641    {
642      try
643      {
644        final InputStream inputStream;
645        final FileArgument inputFileArg =
646             p.getFileArgument(ARG_NAME_INPUT_FILE);
647        if ((inputFileArg != null) && inputFileArg.isPresent())
648        {
649          inputStream = new FileInputStream(inputFileArg.getValue());
650        }
651        else
652        {
653          inputStream = in;
654        }
655
656        final byte[] buffer = new byte[8192];
657        while (true)
658        {
659          final int bytesRead = inputStream.read(buffer);
660          if (bytesRead <= 0)
661          {
662            break;
663          }
664
665          rawDataBuffer.append(buffer, 0, bytesRead);
666        }
667
668        inputStream.close();
669      }
670      catch (final Exception e)
671      {
672        Debug.debugException(e);
673        wrapErr(0, WRAP_COLUMN,
674             "An error occurred while attempting to read the data to encode:  ",
675             StaticUtils.getExceptionMessage(e));
676        return ResultCode.LOCAL_ERROR;
677      }
678    }
679
680
681    // If we should ignore any trailing end-of-line markers, then do that now.
682    final BooleanArgument ignoreEOLArg =
683         p.getBooleanArgument(ARG_NAME_IGNORE_TRAILING_LINE_BREAK);
684    if ((ignoreEOLArg != null) && ignoreEOLArg.isPresent())
685    {
686stripEOLLoop:
687      while (rawDataBuffer.length() > 0)
688      {
689        switch (rawDataBuffer.getBackingArray()[rawDataBuffer.length() - 1])
690        {
691          case '\n':
692          case '\r':
693            rawDataBuffer.delete(rawDataBuffer.length() - 1, 1);
694            break;
695          default:
696            break stripEOLLoop;
697        }
698      }
699    }
700
701
702    // Base64-encode the data.
703    final byte[] rawDataArray = rawDataBuffer.toByteArray();
704    final ByteStringBuffer encodedDataBuffer =
705         new ByteStringBuffer(4 * rawDataBuffer.length() / 3 + 3);
706    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
707    if ((urlArg != null) && urlArg.isPresent())
708    {
709      Base64.urlEncode(rawDataArray, 0, rawDataArray.length, encodedDataBuffer,
710           false);
711    }
712    else
713    {
714      Base64.encode(rawDataArray, encodedDataBuffer);
715    }
716
717
718    // Write the encoded data.
719    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
720    if ((outputFileArg != null) && outputFileArg.isPresent())
721    {
722      try
723      {
724        final FileOutputStream outputStream =
725             new FileOutputStream(outputFileArg.getValue(), false);
726        encodedDataBuffer.write(outputStream);
727        outputStream.write(StaticUtils.EOL_BYTES);
728        outputStream.flush();
729        outputStream.close();
730      }
731      catch (final Exception e)
732      {
733        Debug.debugException(e);
734        wrapErr(0, WRAP_COLUMN,
735             "An error occurred while attempting to write the base64-encoded " +
736                  "data to output file ",
737             outputFileArg.getValue().getAbsolutePath(), ":  ",
738             StaticUtils.getExceptionMessage(e));
739        err("Base64-encoded data:");
740        err(encodedDataBuffer.toString());
741        return ResultCode.LOCAL_ERROR;
742      }
743    }
744    else
745    {
746      out(encodedDataBuffer.toString());
747    }
748
749
750    return ResultCode.SUCCESS;
751  }
752
753
754
755  /**
756   * Performs the necessary work for base64 decoding.
757   *
758   * @param  p  The argument parser for the decode subcommand.
759   *
760   * @return  A result code that indicates whether the processing completed
761   *          successfully.
762   */
763  @NotNull()
764  private ResultCode doDecode(@NotNull final ArgumentParser p)
765  {
766    // Get the data to decode.  We'll always ignore the following:
767    // - Line breaks
768    // - Blank lines
769    // - Lines that start with an octothorpe (#)
770    //
771    // Unless the --url argument was provided, then we'll also ignore lines that
772    // start with a dash (like those used as start and end markers in a
773    // PEM-encoded certificate).  Since dashes are part of the base64url
774    // alphabet, we can't ignore dashes if the --url argument was provided.
775    final ByteStringBuffer encodedDataBuffer = new ByteStringBuffer();
776    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
777    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
778    if ((dataArg != null) && dataArg.isPresent())
779    {
780      encodedDataBuffer.append(dataArg.getValue());
781    }
782    else
783    {
784      try
785      {
786        final BufferedReader reader;
787        final FileArgument inputFileArg =
788             p.getFileArgument(ARG_NAME_INPUT_FILE);
789        if ((inputFileArg != null) && inputFileArg.isPresent())
790        {
791          reader = new BufferedReader(new FileReader(inputFileArg.getValue()));
792        }
793        else
794        {
795          reader = new BufferedReader(new InputStreamReader(in));
796        }
797
798        while (true)
799        {
800          final String line = reader.readLine();
801          if (line == null)
802          {
803            break;
804          }
805
806          if ((line.length() == 0) || line.startsWith("#"))
807          {
808            continue;
809          }
810
811          if (line.startsWith("-") &&
812              ((urlArg == null) || (! urlArg.isPresent())))
813          {
814            continue;
815          }
816
817          encodedDataBuffer.append(line);
818        }
819
820        reader.close();
821      }
822      catch (final Exception e)
823      {
824        Debug.debugException(e);
825        wrapErr(0, WRAP_COLUMN,
826             "An error occurred while attempting to read the data to decode:  ",
827             StaticUtils.getExceptionMessage(e));
828        return ResultCode.LOCAL_ERROR;
829      }
830    }
831
832
833    // Base64-decode the data.
834    final ByteStringBuffer rawDataBuffer = new
835         ByteStringBuffer(encodedDataBuffer.length());
836    if ((urlArg != null) && urlArg.isPresent())
837    {
838      try
839      {
840        rawDataBuffer.append(Base64.urlDecode(encodedDataBuffer.toString()));
841      }
842      catch (final Exception e)
843      {
844        Debug.debugException(e);
845        wrapErr(0, WRAP_COLUMN,
846             "An error occurred while attempting to base64url-decode the " +
847                  "provided data:  " + StaticUtils.getExceptionMessage(e));
848        return ResultCode.LOCAL_ERROR;
849      }
850    }
851    else
852    {
853      try
854      {
855        rawDataBuffer.append(Base64.decode(encodedDataBuffer.toString()));
856      }
857      catch (final Exception e)
858      {
859        Debug.debugException(e);
860        wrapErr(0, WRAP_COLUMN,
861             "An error occurred while attempting to base64-decode the " +
862                  "provided data:  " + StaticUtils.getExceptionMessage(e));
863        return ResultCode.LOCAL_ERROR;
864      }
865    }
866
867
868    // If we should add a newline, then do that now.
869    final BooleanArgument addEOLArg =
870         p.getBooleanArgument(ARG_NAME_ADD_TRAILING_LINE_BREAK);
871    if ((addEOLArg != null) && addEOLArg.isPresent())
872    {
873      rawDataBuffer.append(StaticUtils.EOL_BYTES);
874    }
875
876
877    // Write the decoded data.
878    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
879    if ((outputFileArg != null) && outputFileArg.isPresent())
880    {
881      try
882      {
883        final FileOutputStream outputStream =
884             new FileOutputStream(outputFileArg.getValue(), false);
885        rawDataBuffer.write(outputStream);
886        outputStream.flush();
887        outputStream.close();
888      }
889      catch (final Exception e)
890      {
891        Debug.debugException(e);
892        wrapErr(0, WRAP_COLUMN,
893             "An error occurred while attempting to write the base64-decoded " +
894                  "data to output file ",
895             outputFileArg.getValue().getAbsolutePath(), ":  ",
896             StaticUtils.getExceptionMessage(e));
897        err("Base64-decoded data:");
898        err(encodedDataBuffer.toString());
899        return ResultCode.LOCAL_ERROR;
900      }
901    }
902    else
903    {
904      final byte[] rawDataArray = rawDataBuffer.toByteArray();
905      getOut().write(rawDataArray, 0, rawDataArray.length);
906      getOut().flush();
907    }
908
909
910    return ResultCode.SUCCESS;
911  }
912
913
914
915  /**
916   * Retrieves a set of information that may be used to generate example usage
917   * information.  Each element in the returned map should consist of a map
918   * between an example set of arguments and a string that describes the
919   * behavior of the tool when invoked with that set of arguments.
920   *
921   * @return  A set of information that may be used to generate example usage
922   *          information.  It may be {@code null} or empty if no example usage
923   *          information is available.
924   */
925  @Override()
926  @NotNull()
927  public LinkedHashMap<String[],String> getExampleUsages()
928  {
929    final LinkedHashMap<String[],String> examples =
930         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
931
932    examples.put(
933         new String[]
934         {
935           "encode",
936           "--data", "Hello"
937         },
938         "Base64-encodes the string 'Hello' and writes the result to " +
939              "standard output.");
940
941    examples.put(
942         new String[]
943         {
944           "decode",
945           "--inputFile", "encoded-data.txt",
946           "--outputFile", "decoded-data.txt",
947         },
948         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
949              "and writes the result to the 'raw-data.txt' file.");
950
951    return examples;
952  }
953}