/*
* Copyright 2011 Jin Kwon.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jinahya.rfc4648;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import jinahya.io.BitInput;
import jinahya.io.BitOutput;
/**
*
* @author <a href="mailto:jinahya@gmail.com">Jin Kwon</a>
*/
public abstract class Base {
/** Default pad character. */
protected static final char PAD = '=';
/** MAGIC NUMBER: OCTET SIZE. */
private static final int OCTET_SIZE = 8;
/** MAGIC NUMBER: ASCII SIZE. */
private static final int ASCII_SIZE = 128;
/** MAGIC NUMBER: SMALLEST VISIBLE ASCII. */
private static final int SMALLEST_VISIBLE_ASCII = 33;
/**
* Returns the Least Common Muliple value for given two operands.
*
* @param a the first operand
* @param b the second operand
* @return calculated least common multiple
*/
private static int lcm(final int a, final int b) {
return ((a * b) / gcd(a, b));
}
/**
* Returns the Greatest Common Divisor for given two operands.
*
* @param a the first operand
* @param b the second operand
* @return calculated greate common devisor
*/
private static int gcd(final int a, final int b) {
if (b == 0) {
return a;
} else {
return gcd(b, a % b);
}
}
/**
* Create a new instance.
*
* @param alphabet alphabe to be used
* @param padding flag for padding
*/
protected Base(final byte[] alphabet, final boolean padding) {
super();
if (alphabet == null) {
throw new IllegalArgumentException("null alphabet");
}
if (alphabet.length == 0) {
throw new IllegalArgumentException("empty alphabet");
}
encode = alphabet;
decode = new byte[ASCII_SIZE - SMALLEST_VISIBLE_ASCII + 1];
for (int i = 0; i < decode.length; i++) {
decode[i] = -1;
}
for (byte i = 0; i < encode.length; i++) {
decode[encode[i] - SMALLEST_VISIBLE_ASCII] = i;
}
this.padding = padding;
bitsPerChar = (int) (Math.log(encode.length) / Math.log(2.0d));
bytesPerWord = lcm(OCTET_SIZE, bitsPerChar) / OCTET_SIZE;
charsPerWord = bytesPerWord * OCTET_SIZE / bitsPerChar;
}
/**
*
* @param input
* @return
* @throws IOException
*/
public char[] encode(final byte[] input) throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
return encode(new ByteArrayInputStream(input));
}
/**
*
* @param input
* @return
* @throws IOException
*/
public char[] encode(final InputStream input) throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
final CharArrayWriter output = new CharArrayWriter();
encode(input, output);
output.flush();
return output.toCharArray();
}
/**
*
* @param input binary input
* @param output character output
* @throws IOException if an I/O error occurs
*/
public final void encode(final InputStream input, final Writer output)
throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
if (output == null) {
throw new IllegalArgumentException("null output");
}
encode(new BitInput(input), output);
}
/**
*
* @param input binary input
* @param output character output
* @throws IOException if an I/O error occurs
*/
private void encode(final BitInput input, final Writer output)
throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
if (output == null) {
throw new IllegalArgumentException("null output");
}
outer:
while (true) {
for (int i = 0; i < charsPerWord; i++) {
int available = OCTET_SIZE - ((bitsPerChar * i) % OCTET_SIZE);
if (available >= bitsPerChar) {
try {
int unsigned = input.readUnsignedInt(bitsPerChar);
output.write(encode[unsigned]);
} catch (EOFException eofe) { // i == 0
break outer;
}
} else { // need next octet
int required = bitsPerChar - available;
int unsigned =
(input.readUnsignedInt(available) << required);
try {
unsigned |= input.readUnsignedInt(required);
output.write(encode[unsigned]);
} catch (EOFException eofe) {
output.write(encode[unsigned]);
if (padding) {
for (int j = i + 1; j < charsPerWord; j++) {
output.write(PAD);
}
}
break outer;
}
}
}
}
}
/**
*
* @param input
* @return
* @throws IOException
*/
public final byte[] decode(final char[] input) throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
return decode(new CharArrayReader(input));
}
/**
*
* @param input
* @return
* @throws IOException
*/
public final byte[] decode(final Reader input) throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
final ByteArrayOutputStream output = new ByteArrayOutputStream();
decode(input, output);
output.flush();
return output.toByteArray();
}
/**
*
* @param input character input
* @param output binary output
* @throws IOException if I/O error occurs
*/
public final void decode(final Reader input, final OutputStream output)
throws IOException {
if (input == null) {
throw new IllegalArgumentException("null input");
}
if (output == null) {
throw new IllegalArgumentException("null outpute");
}
decode(input, new BitOutput(output));
}
/**
*
* @param input character input
* @param output binary output
* @throws IOException if I/O error occurs
*/
private void decode(final Reader input, final BitOutput output)
throws IOException {
outer:
while (true) {
int c;
for (int i = 0; i < charsPerWord; i++) {
c = input.read();
if (c == -1) { // end of stream
if (i == 0) { // first character in a word; ok
break outer;
}
if (((i * bitsPerChar) % OCTET_SIZE) >= bitsPerChar) {
throw new EOFException("not finished properly");
}
if (!padding) {
break outer;
}
throw new EOFException("not finished properly");
} else if (c == PAD) {
if (!padding) {
throw new IOException("bad padding; no pads allowed");
}
if (i == 0) { // first character in a word
throw new IOException("bad padding");
}
if (((i * bitsPerChar) % OCTET_SIZE) >= bitsPerChar) {
throw new IOException("bad padding");
}
for (int j = i + 1; j < charsPerWord; j++) {
c = input.read(); // pad
if (c == -1) { // end of stream?
throw new EOFException("not finished properly");
}
if (c != PAD) { // not the pad char?
throw new IOException("bad padding");
}
}
break outer;
} else {
int value = decode[c - SMALLEST_VISIBLE_ASCII];
if (value == -1) {
throw new IOException("bad character: " + (char) c);
}
output.writeUnsignedInt(bitsPerChar, value);
}
}
}
}
/** Characters for encoding. */
private final byte[] encode;
/** Characters for decoding. */
private final byte[] decode;
/** flag for padding. */
private final boolean padding;
/** number of bits per character. */
private final int bitsPerChar;
/** number of bytes per word. */
private final int bytesPerWord;
/** number of characters per word. */
private final int charsPerWord;
}