All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tomgibara.keycode.Keycode Maven / Gradle / Ivy

The newest version!
/*
 *   Copyright 2014 Tom Gibara
 *
 *   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 com.tomgibara.keycode;

import static com.tomgibara.keycode.Encoder.VALUES_32;

import java.io.Serializable;
import java.util.Arrays;

/**
 * 

* Encapsulates 256 bit (32 byte) keys for purpose of parsing & formatting them * into ASCII keycodes. The formatted output is designed to provide brevity * readability and a degree of human verifiability combined with the support of * error detecting codes. *

* Instances of this class are immutable and are designed to be 'cheap' and * 'temporary'. *

* Passing null into any method or constructor in this class will raise a * {@code IllegalArgumentException}. * * @author tomgibara * */ public final class Keycode implements Serializable { private static char appendDigits(StringBuilder sb, int block1, int block2, int block3) { int i = sb.length(); Encoder.append9Bits(sb, block1); Encoder.append9Bits(sb, block2); Encoder.append6Bits(sb, block3); return TAQG10.compute(sb, i, i + 8); } private static String encode(byte[] key) { StringBuilder sb = new StringBuilder(); // initial rows for (int i = 0; i < 30; i += 5) { Encoder.appendBytesBase32(sb, key, i); int length = sb.length(); sb.append(TAQG32.compute(sb, length - 8, length)); } // last row int block1 = ((key[30] & 0xff) << 1) | ((key[31] & 0x80) >> 7); int block2 = ((key[31] & 0x7f) << 2) | ((key[32] & 0xc0) >> 6); int block3 = (key[32] & 0x3f); char c = appendDigits(sb, block1, block2, block3); char k = sb.charAt(61); if (c == k) { block2 ^= 2; // flip the sign-bit of the tag sb.setLength(54); c = appendDigits(sb, block1, block2, block3); } sb.append(c); return sb.toString(); } private static byte[] decode(String str) { // basic checks if (str.length() == 0) throw new IllegalArgumentException("blank code"); if (str.length() < 63) throw new IllegalArgumentException("short code: " + str.length() + " characters"); if (str.length() > 63) throw new IllegalArgumentException("long code: " + str.length() + " characters"); for (int i = 54; i < 62; i++) { // TODO strengthen char c = str.charAt(i); if (c < 48 || c >= 58) throw new IllegalArgumentException("expected digit at character " + (i+1)); } if (str.charAt(54) == '0') throw new IllegalArgumentException("invalid zero at first character of last row"); if (str.charAt(57) == '0') throw new IllegalArgumentException("invalid zero at fourth character of last row"); if (str.charAt(60) == '0') throw new IllegalArgumentException("invalid zero at seventh character of last row"); // checksums for (int i = 0; i < 54; i += 9) { if (!TAQG32.verify(str, i, i + 9)) throw new IllegalArgumentException("invalid checksum for row " + (i / 9 + 1)); } if (!TAQG10.verify(str, 54, 63)) throw new IllegalArgumentException("invalid checksum for last row"); // double digits if ( str.charAt(54) == str.charAt(55) || str.charAt(55) == str.charAt(56) || str.charAt(57) == str.charAt(58) || str.charAt(58) == str.charAt(59) || str.charAt(60) == str.charAt(61) || str.charAt(61) == str.charAt(62) ) { throw new IllegalArgumentException("invalid digit pairs"); } // parsing byte[] key = new byte[33]; for (int i = 0; i < 6; i++) { Encoder.parseBytesBase32(str, i * 9, key, i * 5); } int block1 = Encoder.parse9Bits(str, 54); int block2 = Encoder.parse9Bits(str, 57); int block3 = Encoder.parse6Bits(str, 60); if (block1 >= 512) throw new IllegalArgumentException("invalid first digit triple"); if (block2 >= 512) throw new IllegalArgumentException("invalid second digit triple"); if (block3 >= 64) throw new IllegalArgumentException("invalid third digit triple"); boolean f = (block2 & 2) != 0; if (f) { block2 &= ~2; StringBuilder sb = new StringBuilder(); char c = appendDigits(sb, block1, block2, block3); char k = sb.charAt(7); if (c != k) throw new IllegalArgumentException("invalid tag bit flip"); } key[30] = (byte) ( block1 >> 1 ); key[31] = (byte) ( block1 << 7 | block2 >> 2 ); key[32] = (byte) ( block2 << 6 | block3 ); // all good - return return key; } /** * Defines formatting rules for outputting a keycode to a string. * * @author tomgibara */ public static final class Format implements Serializable { private static boolean isWhitespaceOnly(String str) { for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c >= 128 || Encoder.VALUES_32[c] != -2) { return false; } }; return true; } private static final long serialVersionUID = -7846537134506341028L; private static final Format PLAIN = new Format("", ""); private static final Format STANDARD = new Format(" ", "\n"); private static final Format PLATFORM; static { String s = String.format("%n"); PLATFORM = s.equals(STANDARD.lineSeparator) ? STANDARD : new Format(" ", s); } /** * With this format, the keycode will not include group separators or * line separators. It will consist only of upper case alphanumeric * characters. * * @return a format without whitespace */ public static Format plain() { return PLAIN; } /** * A format in which, the keycode will have newlines separating each * group-triple and a space between each group on a line. This is the * standard format which is expected to provide the best * communicability. * * @return a format with standard whitespace */ public static Format standard() { return STANDARD; } /** * Matches the standard format with the exception that the platform line * separator is used. This format will be identical to that of * {@link #formatStandard()} on platforms which use a single newline as a * line separator. * * @return a format with platform whitespace */ public static Format platform() { return PLATFORM; } /** * Creates a new format with the specified group and line separators. * Separators must only consist of ASCII whitespace, specifically the * characters: ' ', '\t', '\n', '\r' * * @param groupSeparator * the characters inserted between groups * @param lineSeparator * the characters inserted between lines * @return a format with the specified separators */ public static Format custom(String groupSeparator, String lineSeparator) { if (groupSeparator == null) throw new IllegalArgumentException("null groupSeparator"); if (!isWhitespaceOnly(groupSeparator)) throw new IllegalArgumentException("non-whitespace groupSeparator"); if (lineSeparator == null) throw new IllegalArgumentException("null lineSeparator"); if (!isWhitespaceOnly(lineSeparator)) throw new IllegalArgumentException("non-whitespace lineSeparator"); //TODO canonicalize against standard formats? return new Format(groupSeparator, lineSeparator); } final String groupSeparator; final String lineSeparator; private Format(String groupSeparator, String lineSeparator) { this.groupSeparator = groupSeparator; this.lineSeparator = lineSeparator; } public String getGroupSeparator() { return groupSeparator; } public String getLineSeparator() { return lineSeparator; } /** * Encapsulates a 256 bit key for subsequent output as a keycode via the * {@link #Keycode.toString()} method. The tag is implicitly assumed to * be zero. * * @param key * a 32 byte array containing key data * @throws IllegalArgumentException * if the array is not 32 bytes long */ public Keycode keycode(byte[] key) { if (key == null) throw new IllegalArgumentException("null key"); if (key.length != 32) throw new IllegalArgumentException("invalid key length"); byte[] copy = new byte[33]; System.arraycopy(key, 0, copy, 0, 32); return new Keycode(this, copy, encode(copy)); } /** * Encapsulates a 256 bit key for subsequent output as a keycode, * together with a 7 bit tag which is also encoded along with the key. * The tag must be non-negative, that is, only the most-significant-bit * must be zero. * * @param key * a 32 byte array containing key data * @param tag * a 7 bit value that augments the key data * @throws IllegalArgumentException * if the array is not 32 bytes long * @see #getTag() */ public Keycode keycode(byte[] key, byte tag) { if (key == null) throw new IllegalArgumentException("null key"); if (key.length != 32) throw new IllegalArgumentException("invalid key length"); if (tag < 0) throw new IllegalArgumentException("negative tag"); byte[] copy = new byte[33]; System.arraycopy(key, 0, copy, 0, 32); copy[32] = tag; return new Keycode(this, copy, encode(copy)); } /** * Returns a keycode with the same key and tag as an existing keycode, * * @param an existing keycode * @return the same key combined with this format. */ public Keycode keycode(Keycode keycode) { if (keycode == null) throw new IllegalArgumentException("null keycode"); return keycode.format.equals(this) ? keycode : new Keycode(this, keycode.key, keycode.code); } /** *

* Parses a keycode from character data. *

* Note: The parse method is not strict, in the sense that any * whitespace group and line separators will be processed, irrespective * of the format used to perform the parsing. * * * @param code * the character data of the code, typically a String * @throws IllegalArgumentException * if the code contains non-whitespace, non-code characters * OR has an invalid structure OR a data error is detected * @return a successfully parsed keycode with this format */ public Keycode parse(CharSequence code) { if (code == null) throw new IllegalArgumentException("null code"); if (code.length() == 0) throw new IllegalArgumentException("empty code"); // eliminate whitespace and check characters StringBuilder sb = null; int last = 0; int codeLength = code.length(); for (int i = 0; i < codeLength; i++) { char c = code.charAt(i); if (c >= 128) throw new IllegalArgumentException("non-ascii character at " + (i + 1)); int value = VALUES_32[c]; switch (value) { case -2: if (last != i) { if (sb == null) sb = new StringBuilder(63); sb.append(code, last, i); } last = i + 1; continue; case -1: throw new IllegalArgumentException("invalid character at " + (i + 1)); default: continue; } } // convert to a String and decode //note: risk that char sequence will not return same characters that were checked String str = sb == null ? code.toString() : sb.append(code, last, codeLength).toString(); byte[] key = decode(str); // done return new Keycode(this, key, str); } @Override public int hashCode() { return groupSeparator.hashCode() * 31 ^ lineSeparator.hashCode(); } /** * Two formats are equal if they produce identical output over all * possible keys. */ @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof Format)) return false; Format that = (Format) obj; if (!this.groupSeparator.equals(that.groupSeparator)) return false; if (!this.lineSeparator.equals(that.lineSeparator)) return false; return true; } private Object readResolve() { if (this.equals(STANDARD)) return STANDARD; if (this.equals(PLAIN)) return PLAIN; if (this.equals(PLATFORM)) return PLATFORM; return this; } } private static final long serialVersionUID = -8610389751205547848L; private final Format format; private final byte[] key; private String code; private Keycode(Format format, byte[] key, String code) { this.format = format; this.key = key; this.code = code; } /** * The format which controls the output of {@link #toString()} * * @return the keycode format */ public Format getFormat() { return format; } /** * The key encapsulated by this object. * * @return a 32 byte array containing a 256 bit key */ public byte[] getKey() { return Arrays.copyOf(key, 32); } /** * The tag associated with this key. The tag may be used to distinguish * multiple keys which are being supplied as part of a single message. * Alternatively the tag may be used as part of an additional application * specific checksum. * * @return the tag associated with the key, non-negative, typically zero */ public byte getTag() { return key[32]; } @Override public int hashCode() { return Arrays.hashCode(key) ^ format.hashCode(); } /** * Two keycodes are equal if they encapsulate the same key and produce the * same output. */ @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof Keycode)) return false; Keycode that = (Keycode) obj; if (!Arrays.equals(this.key, that.key)) return false; if (!this.format.equals(that.format)) return false; return true; } /** * Formats a key into a @code{String} using the keycode format. * * @param format * controls the formatting of the output * @see #getFormat() * @return a string containing the coded key */ @Override public String toString() { String lineSep = format.lineSeparator; String groupSep = format.groupSeparator; boolean noLines = lineSep.isEmpty(); boolean noGroups = groupSep.isEmpty(); if (noLines && noGroups) return code; StringBuilder sb = new StringBuilder(63 + 6 * lineSep.length() + 14 * groupSep.length()); for (int i = 0; i < 63; i+= 9) { if (!noLines && i > 0) sb.append(lineSep); if (noGroups) { sb.append(code.substring(i , i + 9)); } else { sb.append(code.substring(i , i + 3)); sb.append(groupSep); sb.append(code.substring(i + 3, i + 6)); sb.append(groupSep); sb.append(code.substring(i + 6, i + 9)); } } return sb.toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy