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}