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