001/* 002 * Copyright 2016-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2016-2019 Ping Identity Corporation 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 */ 021package com.unboundid.ldap.sdk.examples; 022 023 024 025import java.io.BufferedReader; 026import java.io.FileInputStream; 027import java.io.FileReader; 028import java.io.FileOutputStream; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.OutputStream; 032import java.util.LinkedHashMap; 033 034import com.unboundid.ldap.sdk.ResultCode; 035import com.unboundid.ldap.sdk.Version; 036import com.unboundid.util.Base64; 037import com.unboundid.util.ByteStringBuffer; 038import com.unboundid.util.CommandLineTool; 039import com.unboundid.util.Debug; 040import com.unboundid.util.StaticUtils; 041import com.unboundid.util.ThreadSafety; 042import com.unboundid.util.ThreadSafetyLevel; 043import com.unboundid.util.args.ArgumentException; 044import com.unboundid.util.args.ArgumentParser; 045import com.unboundid.util.args.BooleanArgument; 046import com.unboundid.util.args.FileArgument; 047import com.unboundid.util.args.StringArgument; 048import 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) 075public 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 return true; 283 } 284 285 286 287 /** 288 * Indicates whether this tool defaults to launching in interactive mode if 289 * the tool is invoked without any command-line arguments. This will only be 290 * used if {@link #supportsInteractiveMode()} returns {@code true}. 291 * 292 * @return {@code true} if this tool defaults to using interactive mode if 293 * launched without any command-line arguments, or {@code false} if 294 * not. 295 */ 296 @Override() 297 public boolean defaultsToInteractiveMode() 298 { 299 return true; 300 } 301 302 303 304 /** 305 * Indicates whether this tool supports the use of a properties file for 306 * specifying default values for arguments that aren't specified on the 307 * command line. 308 * 309 * @return {@code true} if this tool supports the use of a properties file 310 * for specifying default values for arguments that aren't specified 311 * on the command line, or {@code false} if not. 312 */ 313 @Override() 314 public boolean supportsPropertiesFile() 315 { 316 return true; 317 } 318 319 320 321 /** 322 * Indicates whether this tool should provide arguments for redirecting output 323 * to a file. If this method returns {@code true}, then the tool will offer 324 * an "--outputFile" argument that will specify the path to a file to which 325 * all standard output and standard error content will be written, and it will 326 * also offer a "--teeToStandardOut" argument that can only be used if the 327 * "--outputFile" argument is present and will cause all output to be written 328 * to both the specified output file and to standard output. 329 * 330 * @return {@code true} if this tool should provide arguments for redirecting 331 * output to a file, or {@code false} if not. 332 */ 333 @Override() 334 protected boolean supportsOutputFile() 335 { 336 // This tool provides its own output file support. 337 return false; 338 } 339 340 341 342 /** 343 * Adds the command-line arguments supported for use with this tool to the 344 * provided argument parser. The tool may need to retain references to the 345 * arguments (and/or the argument parser, if trailing arguments are allowed) 346 * to it in order to obtain their values for use in later processing. 347 * 348 * @param parser The argument parser to which the arguments are to be added. 349 * 350 * @throws ArgumentException If a problem occurs while adding any of the 351 * tool-specific arguments to the provided 352 * argument parser. 353 */ 354 @Override() 355 public void addToolArguments(final ArgumentParser parser) 356 throws ArgumentException 357 { 358 this.parser = parser; 359 360 361 // Create the subcommand for encoding data. 362 final ArgumentParser encodeParser = 363 new ArgumentParser("encode", "Base64-encodes raw data."); 364 365 final StringArgument encodeDataArgument = new StringArgument('d', 366 ARG_NAME_DATA, false, 1, "{data}", 367 "The raw data to be encoded. If neither the --" + ARG_NAME_DATA + 368 " nor the --" + ARG_NAME_INPUT_FILE + " argument is provided, " + 369 "then the data will be read from standard input."); 370 encodeDataArgument.addLongIdentifier("rawData", true); 371 encodeDataArgument.addLongIdentifier("raw-data", true); 372 encodeParser.addArgument(encodeDataArgument); 373 374 final FileArgument encodeDataFileArgument = new FileArgument('f', 375 ARG_NAME_INPUT_FILE, false, 1, null, 376 "The path to a file containing the raw data to be encoded. If " + 377 "neither the --" + ARG_NAME_DATA + " nor the --" + 378 ARG_NAME_INPUT_FILE + " argument is provided, then the data " + 379 "will be read from standard input.", 380 true, true, true, false); 381 encodeDataFileArgument.addLongIdentifier("rawDataFile", true); 382 encodeDataFileArgument.addLongIdentifier("input-file", true); 383 encodeDataFileArgument.addLongIdentifier("raw-data-file", true); 384 encodeParser.addArgument(encodeDataFileArgument); 385 386 final FileArgument encodeOutputFileArgument = new FileArgument('o', 387 ARG_NAME_OUTPUT_FILE, false, 1, null, 388 "The path to a file to which the encoded data should be written. " + 389 "If this is not provided, the encoded data will be written to " + 390 "standard output.", 391 false, true, true, false); 392 encodeOutputFileArgument.addLongIdentifier("toEncodedFile", true); 393 encodeOutputFileArgument.addLongIdentifier("output-file", true); 394 encodeOutputFileArgument.addLongIdentifier("to-encoded-file", true); 395 encodeParser.addArgument(encodeOutputFileArgument); 396 397 final BooleanArgument encodeURLArgument = new BooleanArgument(null, 398 ARG_NAME_URL, 399 "Encode the data with the base64url mechanism rather than the " + 400 "standard base64 mechanism."); 401 encodeParser.addArgument(encodeURLArgument); 402 403 final BooleanArgument encodeIgnoreTrailingEOLArgument = new BooleanArgument( 404 null, ARG_NAME_IGNORE_TRAILING_LINE_BREAK, 405 "Ignore any end-of-line marker that may be present at the end of " + 406 "the data to encode."); 407 encodeIgnoreTrailingEOLArgument.addLongIdentifier( 408 "ignore-trailing-line-break", true); 409 encodeParser.addArgument(encodeIgnoreTrailingEOLArgument); 410 411 encodeParser.addExclusiveArgumentSet(encodeDataArgument, 412 encodeDataFileArgument); 413 414 final LinkedHashMap<String[],String> encodeExamples = 415 new LinkedHashMap<>(StaticUtils.computeMapCapacity(3)); 416 encodeExamples.put( 417 new String[] 418 { 419 "encode", 420 "--data", "Hello" 421 }, 422 "Base64-encodes the string 'Hello' and writes the result to " + 423 "standard output."); 424 encodeExamples.put( 425 new String[] 426 { 427 "encode", 428 "--inputFile", "raw-data.txt", 429 "--outputFile", "encoded-data.txt", 430 }, 431 "Base64-encodes the data contained in the 'raw-data.txt' file and " + 432 "writes the result to the 'encoded-data.txt' file."); 433 encodeExamples.put( 434 new String[] 435 { 436 "encode" 437 }, 438 "Base64-encodes data read from standard input and writes the result " + 439 "to standard output."); 440 441 final SubCommand encodeSubCommand = new SubCommand(SUBCOMMAND_NAME_ENCODE, 442 "Base64-encodes raw data.", encodeParser, encodeExamples); 443 parser.addSubCommand(encodeSubCommand); 444 445 446 // Create the subcommand for decoding data. 447 final ArgumentParser decodeParser = 448 new ArgumentParser("decode", "Decodes base64-encoded data."); 449 450 final StringArgument decodeDataArgument = new StringArgument('d', 451 ARG_NAME_DATA, false, 1, "{data}", 452 "The base64-encoded data to be decoded. If neither the --" + 453 ARG_NAME_DATA + " nor the --" + ARG_NAME_INPUT_FILE + 454 " argument is provided, then the data will be read from " + 455 "standard input."); 456 decodeDataArgument.addLongIdentifier("encodedData", true); 457 decodeDataArgument.addLongIdentifier("encoded-data", true); 458 decodeParser.addArgument(decodeDataArgument); 459 460 final FileArgument decodeDataFileArgument = new FileArgument('f', 461 ARG_NAME_INPUT_FILE, false, 1, null, 462 "The path to a file containing the base64-encoded data to be " + 463 "decoded. If neither the --" + ARG_NAME_DATA + " nor the --" + 464 ARG_NAME_INPUT_FILE + " argument is provided, then the data " + 465 "will be read from standard input.", 466 true, true, true, false); 467 decodeDataFileArgument.addLongIdentifier("encodedDataFile", true); 468 decodeDataFileArgument.addLongIdentifier("input-file", true); 469 decodeDataFileArgument.addLongIdentifier("encoded-data-file", true); 470 decodeParser.addArgument(decodeDataFileArgument); 471 472 final FileArgument decodeOutputFileArgument = new FileArgument('o', 473 ARG_NAME_OUTPUT_FILE, false, 1, null, 474 "The path to a file to which the decoded data should be written. " + 475 "If this is not provided, the decoded data will be written to " + 476 "standard output.", 477 false, true, true, false); 478 decodeOutputFileArgument.addLongIdentifier("toRawFile", true); 479 decodeOutputFileArgument.addLongIdentifier("output-file", true); 480 decodeOutputFileArgument.addLongIdentifier("to-raw-file", true); 481 decodeParser.addArgument(decodeOutputFileArgument); 482 483 final BooleanArgument decodeURLArgument = new BooleanArgument(null, 484 ARG_NAME_URL, 485 "Decode the data with the base64url mechanism rather than the " + 486 "standard base64 mechanism."); 487 decodeParser.addArgument(decodeURLArgument); 488 489 final BooleanArgument decodeAddTrailingLineBreak = new BooleanArgument( 490 null, ARG_NAME_ADD_TRAILING_LINE_BREAK, 491 "Add a line break to the end of the decoded data."); 492 decodeAddTrailingLineBreak.addLongIdentifier("add-trailing-line-break", 493 true); 494 decodeParser.addArgument(decodeAddTrailingLineBreak); 495 496 decodeParser.addExclusiveArgumentSet(decodeDataArgument, 497 decodeDataFileArgument); 498 499 final LinkedHashMap<String[],String> decodeExamples = 500 new LinkedHashMap<>(StaticUtils.computeMapCapacity(3)); 501 decodeExamples.put( 502 new String[] 503 { 504 "decode", 505 "--data", "SGVsbG8=" 506 }, 507 "Base64-decodes the string 'SGVsbG8=' and writes the result to " + 508 "standard output."); 509 decodeExamples.put( 510 new String[] 511 { 512 "decode", 513 "--inputFile", "encoded-data.txt", 514 "--outputFile", "decoded-data.txt", 515 }, 516 "Base64-decodes the data contained in the 'encoded-data.txt' file " + 517 "and writes the result to the 'raw-data.txt' file."); 518 decodeExamples.put( 519 new String[] 520 { 521 "decode" 522 }, 523 "Base64-decodes data read from standard input and writes the result " + 524 "to standard output."); 525 526 final SubCommand decodeSubCommand = new SubCommand(SUBCOMMAND_NAME_DECODE, 527 "Decodes base64-encoded data.", decodeParser, decodeExamples); 528 parser.addSubCommand(decodeSubCommand); 529 } 530 531 532 533 /** 534 * Performs the core set of processing for this tool. 535 * 536 * @return A result code that indicates whether the processing completed 537 * successfully. 538 */ 539 @Override() 540 public ResultCode doToolProcessing() 541 { 542 // Get the subcommand selected by the user. 543 final SubCommand subCommand = parser.getSelectedSubCommand(); 544 if (subCommand == null) 545 { 546 // This should never happen. 547 wrapErr(0, WRAP_COLUMN, "No subcommand was selected."); 548 return ResultCode.PARAM_ERROR; 549 } 550 551 552 // Take the appropriate action based on the selected subcommand. 553 if (subCommand.hasName(SUBCOMMAND_NAME_ENCODE)) 554 { 555 return doEncode(subCommand.getArgumentParser()); 556 } 557 else 558 { 559 return doDecode(subCommand.getArgumentParser()); 560 } 561 } 562 563 564 565 /** 566 * Performs the necessary work for base64 encoding. 567 * 568 * @param p The argument parser for the encode subcommand. 569 * 570 * @return A result code that indicates whether the processing completed 571 * successfully. 572 */ 573 private ResultCode doEncode(final ArgumentParser p) 574 { 575 // Get the data to encode. 576 final ByteStringBuffer rawDataBuffer = new ByteStringBuffer(); 577 final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA); 578 if ((dataArg != null) && dataArg.isPresent()) 579 { 580 rawDataBuffer.append(dataArg.getValue()); 581 } 582 else 583 { 584 try 585 { 586 final InputStream inputStream; 587 final FileArgument inputFileArg = 588 p.getFileArgument(ARG_NAME_INPUT_FILE); 589 if ((inputFileArg != null) && inputFileArg.isPresent()) 590 { 591 inputStream = new FileInputStream(inputFileArg.getValue()); 592 } 593 else 594 { 595 inputStream = in; 596 } 597 598 final byte[] buffer = new byte[8192]; 599 while (true) 600 { 601 final int bytesRead = inputStream.read(buffer); 602 if (bytesRead <= 0) 603 { 604 break; 605 } 606 607 rawDataBuffer.append(buffer, 0, bytesRead); 608 } 609 610 inputStream.close(); 611 } 612 catch (final Exception e) 613 { 614 Debug.debugException(e); 615 wrapErr(0, WRAP_COLUMN, 616 "An error occurred while attempting to read the data to encode: ", 617 StaticUtils.getExceptionMessage(e)); 618 return ResultCode.LOCAL_ERROR; 619 } 620 } 621 622 623 // If we should ignore any trailing end-of-line markers, then do that now. 624 final BooleanArgument ignoreEOLArg = 625 p.getBooleanArgument(ARG_NAME_IGNORE_TRAILING_LINE_BREAK); 626 if ((ignoreEOLArg != null) && ignoreEOLArg.isPresent()) 627 { 628stripEOLLoop: 629 while (rawDataBuffer.length() > 0) 630 { 631 switch (rawDataBuffer.getBackingArray()[rawDataBuffer.length() - 1]) 632 { 633 case '\n': 634 case '\r': 635 rawDataBuffer.delete(rawDataBuffer.length() - 1, 1); 636 break; 637 default: 638 break stripEOLLoop; 639 } 640 } 641 } 642 643 644 // Base64-encode the data. 645 final byte[] rawDataArray = rawDataBuffer.toByteArray(); 646 final ByteStringBuffer encodedDataBuffer = 647 new ByteStringBuffer(4 * rawDataBuffer.length() / 3 + 3); 648 final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL); 649 if ((urlArg != null) && urlArg.isPresent()) 650 { 651 Base64.urlEncode(rawDataArray, 0, rawDataArray.length, encodedDataBuffer, 652 false); 653 } 654 else 655 { 656 Base64.encode(rawDataArray, encodedDataBuffer); 657 } 658 659 660 // Write the encoded data. 661 final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE); 662 if ((outputFileArg != null) && outputFileArg.isPresent()) 663 { 664 try 665 { 666 final FileOutputStream outputStream = 667 new FileOutputStream(outputFileArg.getValue(), false); 668 encodedDataBuffer.write(outputStream); 669 outputStream.write(StaticUtils.EOL_BYTES); 670 outputStream.flush(); 671 outputStream.close(); 672 } 673 catch (final Exception e) 674 { 675 Debug.debugException(e); 676 wrapErr(0, WRAP_COLUMN, 677 "An error occurred while attempting to write the base64-encoded " + 678 "data to output file ", 679 outputFileArg.getValue().getAbsolutePath(), ": ", 680 StaticUtils.getExceptionMessage(e)); 681 err("Base64-encoded data:"); 682 err(encodedDataBuffer.toString()); 683 return ResultCode.LOCAL_ERROR; 684 } 685 } 686 else 687 { 688 out(encodedDataBuffer.toString()); 689 } 690 691 692 return ResultCode.SUCCESS; 693 } 694 695 696 697 /** 698 * Performs the necessary work for base64 decoding. 699 * 700 * @param p The argument parser for the decode subcommand. 701 * 702 * @return A result code that indicates whether the processing completed 703 * successfully. 704 */ 705 private ResultCode doDecode(final ArgumentParser p) 706 { 707 // Get the data to decode. We'll always ignore the following: 708 // - Line breaks 709 // - Blank lines 710 // - Lines that start with an octothorpe (#) 711 // 712 // Unless the --url argument was provided, then we'll also ignore lines that 713 // start with a dash (like those used as start and end markers in a 714 // PEM-encoded certificate). Since dashes are part of the base64url 715 // alphabet, we can't ignore dashes if the --url argument was provided. 716 final ByteStringBuffer encodedDataBuffer = new ByteStringBuffer(); 717 final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL); 718 final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA); 719 if ((dataArg != null) && dataArg.isPresent()) 720 { 721 encodedDataBuffer.append(dataArg.getValue()); 722 } 723 else 724 { 725 try 726 { 727 final BufferedReader reader; 728 final FileArgument inputFileArg = 729 p.getFileArgument(ARG_NAME_INPUT_FILE); 730 if ((inputFileArg != null) && inputFileArg.isPresent()) 731 { 732 reader = new BufferedReader(new FileReader(inputFileArg.getValue())); 733 } 734 else 735 { 736 reader = new BufferedReader(new InputStreamReader(in)); 737 } 738 739 while (true) 740 { 741 final String line = reader.readLine(); 742 if (line == null) 743 { 744 break; 745 } 746 747 if ((line.length() == 0) || line.startsWith("#")) 748 { 749 continue; 750 } 751 752 if (line.startsWith("-") && 753 ((urlArg == null) || (! urlArg.isPresent()))) 754 { 755 continue; 756 } 757 758 encodedDataBuffer.append(line); 759 } 760 761 reader.close(); 762 } 763 catch (final Exception e) 764 { 765 Debug.debugException(e); 766 wrapErr(0, WRAP_COLUMN, 767 "An error occurred while attempting to read the data to decode: ", 768 StaticUtils.getExceptionMessage(e)); 769 return ResultCode.LOCAL_ERROR; 770 } 771 } 772 773 774 // Base64-decode the data. 775 final ByteStringBuffer rawDataBuffer = new 776 ByteStringBuffer(encodedDataBuffer.length()); 777 if ((urlArg != null) && urlArg.isPresent()) 778 { 779 try 780 { 781 rawDataBuffer.append(Base64.urlDecode(encodedDataBuffer.toString())); 782 } 783 catch (final Exception e) 784 { 785 Debug.debugException(e); 786 wrapErr(0, WRAP_COLUMN, 787 "An error occurred while attempting to base64url-decode the " + 788 "provided data: " + StaticUtils.getExceptionMessage(e)); 789 return ResultCode.LOCAL_ERROR; 790 } 791 } 792 else 793 { 794 try 795 { 796 rawDataBuffer.append(Base64.decode(encodedDataBuffer.toString())); 797 } 798 catch (final Exception e) 799 { 800 Debug.debugException(e); 801 wrapErr(0, WRAP_COLUMN, 802 "An error occurred while attempting to base64-decode the " + 803 "provided data: " + StaticUtils.getExceptionMessage(e)); 804 return ResultCode.LOCAL_ERROR; 805 } 806 } 807 808 809 // If we should add a newline, then do that now. 810 final BooleanArgument addEOLArg = 811 p.getBooleanArgument(ARG_NAME_ADD_TRAILING_LINE_BREAK); 812 if ((addEOLArg != null) && addEOLArg.isPresent()) 813 { 814 rawDataBuffer.append(StaticUtils.EOL_BYTES); 815 } 816 817 818 // Write the decoded data. 819 final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE); 820 if ((outputFileArg != null) && outputFileArg.isPresent()) 821 { 822 try 823 { 824 final FileOutputStream outputStream = 825 new FileOutputStream(outputFileArg.getValue(), false); 826 rawDataBuffer.write(outputStream); 827 outputStream.flush(); 828 outputStream.close(); 829 } 830 catch (final Exception e) 831 { 832 Debug.debugException(e); 833 wrapErr(0, WRAP_COLUMN, 834 "An error occurred while attempting to write the base64-decoded " + 835 "data to output file ", 836 outputFileArg.getValue().getAbsolutePath(), ": ", 837 StaticUtils.getExceptionMessage(e)); 838 err("Base64-decoded data:"); 839 err(encodedDataBuffer.toString()); 840 return ResultCode.LOCAL_ERROR; 841 } 842 } 843 else 844 { 845 final byte[] rawDataArray = rawDataBuffer.toByteArray(); 846 getOut().write(rawDataArray, 0, rawDataArray.length); 847 getOut().flush(); 848 } 849 850 851 return ResultCode.SUCCESS; 852 } 853 854 855 856 /** 857 * Retrieves a set of information that may be used to generate example usage 858 * information. Each element in the returned map should consist of a map 859 * between an example set of arguments and a string that describes the 860 * behavior of the tool when invoked with that set of arguments. 861 * 862 * @return A set of information that may be used to generate example usage 863 * information. It may be {@code null} or empty if no example usage 864 * information is available. 865 */ 866 @Override() 867 public LinkedHashMap<String[],String> getExampleUsages() 868 { 869 final LinkedHashMap<String[],String> examples = 870 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 871 872 examples.put( 873 new String[] 874 { 875 "encode", 876 "--data", "Hello" 877 }, 878 "Base64-encodes the string 'Hello' and writes the result to " + 879 "standard output."); 880 881 examples.put( 882 new String[] 883 { 884 "decode", 885 "--inputFile", "encoded-data.txt", 886 "--outputFile", "decoded-data.txt", 887 }, 888 "Base64-decodes the data contained in the 'encoded-data.txt' file " + 889 "and writes the result to the 'raw-data.txt' file."); 890 891 return examples; 892 } 893}