/* * ID3Util.java * * Created on 2-Jan-2004 * * Copyright (C)2004,2005 Paul Grebenc * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * $Id: ID3Util.java,v 1.21 2005/02/06 18:11:25 paul Exp $ */ package org.blinkenlights.jid3.util; import java.io.*; import java.net.*; import java.util.*; import java.util.jar.*; import org.blinkenlights.jid3.*; import org.blinkenlights.jid3.v1.*; import org.blinkenlights.jid3.v2.*; /** * @author paul * * A collection of random utility functions used throughout the project. * */ public class ID3Util { /** License. */ private static final String LICENSE = "Copyright (C)2003-2005 Paul Grebenc\n\n" + "This library is free software; you can redistribute it and/or\n" + "modify it under the terms of the GNU Lesser General Public\n" + "License as published by the Free Software Foundation; either\n" + "version 2.1 of the License, or (at your option) any later version.\n\n" + "This library is distributed in the hope that it will be useful,\n" + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" + "Lesser General Public License for more details.\n\n" + "You should have received a copy of the GNU Lesser General Public\n" + "License along with this library; if not, write to the Free Software\n" + "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA"; /** Get the version of this library. */ public static String getVersion() { try { // read library version string from properties file InputStream oPropIS = ID3Util.class.getResourceAsStream("/jid3.properties"); Properties oProperties = new Properties(); oProperties.load(oPropIS); String sVersion = oProperties.getProperty("jid3.version"); // date timestamp of properties file // get properties file as URL URL oJID3URL = ID3Util.class.getResource("/jid3.properties"); // turn URL into a filename String sJarFile = oJID3URL.toExternalForm(); sJarFile = sJarFile.replaceFirst("^jar:/?/?", ""); sJarFile = sJarFile.replaceFirst("^file:", ""); sJarFile = sJarFile.replaceFirst("!.*$", ""); // open jar file to get at the properties file and check its time JarFile oJarFile = new JarFile(sJarFile); JarEntry oJarEntry = oJarFile.getJarEntry("jid3.properties"); long lLastModified = oJarEntry.getTime(); Date oDate = new Date(lLastModified); return sVersion + " (" + oDate.toString() + ")"; } catch (Exception e) { return e.toString(); } } /** Get the license for this library. */ public static String getLicense() { return LICENSE; } public static void main(String[] args) { System.out.println("JID3 library version " + getVersion() + "\n\n" + getLicense()); } /** Utility method to report all tags found by printing them to stdout. * * @param aoID3Tag an array of ID3Tag objects * @throws Exception */ public static void printTags(ID3Tag[] aoID3Tag) throws Exception { System.out.println("Number of tag sets: " + aoID3Tag.length); for (int i=0; i < aoID3Tag.length; i++) { if (aoID3Tag[i] instanceof ID3V1_0Tag) { System.out.println("ID3V1_0Tag:"); System.out.println(aoID3Tag[i].toString()); } else if (aoID3Tag[i] instanceof ID3V1_1Tag) { System.out.println("ID3V1_1Tag:"); System.out.println(aoID3Tag[i].toString()); } else if (aoID3Tag[i] instanceof ID3V2_3_0Tag) { System.out.println("ID3V2_3_0Tag:"); System.out.println(aoID3Tag[i].toString()); } } } /** Check if a byte array will require unsynchronization before being written as a tag. * If the byte array contains any $FF bytes, then it will require unsynchronization. * * @param abySource the byte array to be examined * @return true if unsynchronization is required, false otherwise */ public static boolean requiresUnsynchronization(byte[] abySource) { for (int i=0; i < abySource.length-1; i++) { if (((abySource[i] & 0xff) == 0xff) && ((abySource[i+1] & 0xff) >= 224)) { return true; } } return false; } /** Unsynchronize an array of bytes. * In order to prevent a media player from incorrectly interpreting the contents of a tag, all $FF bytes * followed by a byte with value >=224 must be followed by a $00 byte (thus, $FF $F0 sequences become $FF $00 $F0). * * NOTE: Unsynchronization is not always necessary, if no false sync patterns exist in the data. Only if the * length of the returned byte array is greater than that of the source array, was an unsynchronization * modification applied. * * @param abySource a byte array to be unsynchronized * @return a unsynchronized representation of the source */ public static byte[] unsynchronize(byte[] abySource) { ByteArrayInputStream oBAIS = new ByteArrayInputStream(abySource); ByteArrayOutputStream oBAOS = new ByteArrayOutputStream(); boolean bUnsynchronizationUsed = false; while (oBAIS.available() > 0) { int iVal = oBAIS.read(); oBAOS.write(iVal); if (iVal == 0xff) { // if byte is $FF, we must check the following byte if there is one if (oBAIS.available() > 0) { oBAIS.mark(1); // remember where we were, if we don't need to unsynchronize int iNextVal = oBAIS.read(); if (iNextVal >= 224) { // we need to unsynchronize here oBAOS.write(0); oBAOS.write(iNextVal); bUnsynchronizationUsed = true; } else { oBAIS.reset(); } } } } // if we needed to unsynchronize anything, and this tag ends with 0xff, we have to append a zero byte, // which will be removed on de-unsynchronization later if (bUnsynchronizationUsed && ((abySource[abySource.length-1] & 0xff) == 0xff)) { oBAOS.write(0); } return oBAOS.toByteArray(); } /** De-unsynchronize an array of bytes. * This method takes an array of bytes which has already been unsynchronized, and it reverses * this process, returning the original unencoded array of bytes. * * NOTE: De-unsynchronizing a byte array which was not unsynchronized can cause data corruption. * * @param abySource a byte array to be unencoded * @return the original byte array */ public static byte[] deunsynchronize(byte[] abySource) { ByteArrayInputStream oBAIS = new ByteArrayInputStream(abySource); ByteArrayOutputStream oBAOS = new ByteArrayOutputStream(); while (oBAIS.available() > 0) { int iVal = oBAIS.read(); oBAOS.write(iVal); if (iVal == 0xff) { // we are skipping (what should be) a $00 byte (otherwise, GIGO) oBAIS.read(); } } return oBAOS.toByteArray(); } /** A utility method which, given an array of bytes, will return a character string containing * the hex values of the bytes, optionally separated by colons (ie. 2F:01:A9:3C:etc.), which * may be useful in debugging output or elsewhere. * @param abyRawBytes the byte array to be converted * @param bIncludeColons whether to include colons in output string or not * @return a string containing a hex representation of the byte array */ public static String convertBytesToHexString(byte[] abyRawBytes, boolean bIncludeColons) { StringBuffer sbBuffer = new StringBuffer(); for (int iNum = 0; iNum < abyRawBytes.length; iNum++) { int iVal; if (abyRawBytes[iNum] < 0) { iVal = abyRawBytes[iNum] + 256; } else { iVal = abyRawBytes[iNum]; } String sHexVal = Integer.toHexString(iVal); if (sHexVal.length() == 1) { sbBuffer.append("0"); } sbBuffer.append(Integer.toHexString(iVal)); if ((bIncludeColons) && (iNum < (abyRawBytes.length - 1))) { sbBuffer.append(":"); } } return sbBuffer.toString(); } /** Copy a file. * * @param sSource source filename * @param sDestination destination filename * @throws Exception */ public static void copy(String sSource, String sDestination) throws Exception { FileInputStream oFIS = null; FileOutputStream oFOS = null; try { oFIS = new FileInputStream(sSource); oFOS = new FileOutputStream(sDestination); byte[] abyBuffer = new byte[16384]; int iNumRead; while ((iNumRead = oFIS.read(abyBuffer)) != -1) { oFOS.write(abyBuffer, 0, iNumRead); } oFOS.flush(); } finally { try { oFIS.close(); } catch (Exception e) {} try { oFOS.close(); } catch (Exception e) {} } } /** Compare two files. * * @param sFileOne filename * @param sFileTwo filename * @return true if identical, false otherwise * @throws Exception */ public static void compare(String sFileOne, String sFileTwo) throws Exception { File oOneFile = new File(sFileOne); File oTwoFile = new File(sFileTwo); // check that lengths are the same if (oOneFile.length() != oTwoFile.length()) { throw new Exception("File lengths differ."); } FileInputStream oFIS1 = new FileInputStream(oOneFile); FileInputStream oFIS2 = new FileInputStream(oTwoFile); try { int c; // lengths are equal, so check that contents are the same int i=0; while ((c = oFIS1.read()) != -1) { if (oFIS2.read() != c) { throw new Exception("File contents differ at position " + i + "."); } i++; } } finally { oFIS1.close(); oFIS2.close(); } } /** Convert a clipboard buffer copied from the frhed hex editor, to an array of bytes. This method is used * in test cases, to compare the results of the test with the expected values (as verified in frhed). * * @param sInput the content of the clipboard buffer (in frhed format) * @return a byte array representing the input string * @throws Exception if there is either an error parsing the input string */ public static byte[] convertFrhedToByteArray(String sInput) throws Exception { ByteArrayOutputStream oBAOS = new ByteArrayOutputStream(); StringReader oSR = new StringReader(sInput); int iChar; while ((iChar = oSR.read()) != -1) { char c = (char)iChar; if (c == '\\') { // this is an escaped character, so copy the next one over unmodified iChar = oSR.read(); oBAOS.write(iChar); } else if (c == '<') { // copy string up to colon StringBuffer sbEncoding = new StringBuffer(); while ((iChar = oSR.read()) != ':') { sbEncoding.append((char)iChar); } String sEncoding = sbEncoding.toString(); if (sEncoding.equals("bh")) { // read value up to closing right angle bracket StringBuffer sbValue = new StringBuffer(); while ((iChar = oSR.read()) != '>') { sbValue.append((char)iChar); } String sValue = sbValue.toString(); // convert value from hex string to byte oBAOS.write(Integer.parseInt(sValue, 16)); } else { throw new Exception("Unknown encoding type: " + sEncoding); } } else { // copy value over directly oBAOS.write(iChar); } } return oBAOS.toByteArray(); } /** Compare the starting bytes of a file against a given byte array. Used for testing. * This method returning nothing if successful. * * @param oFile the file to verify * @param abyExpected an array of bytes which are expected to start the specified file * @throws Exception if the contents do not match * @throws IOException if there was an error reading the file */ public static void compareFilePrefix(File oFile, byte[] abyExpected) throws Exception { InputStream oBIS = new BufferedInputStream(new FileInputStream(oFile)); try { ByteArrayOutputStream oBAOS = new ByteArrayOutputStream(); ByteArrayInputStream oBAIS = new ByteArrayInputStream(abyExpected); for (int i=0; i < abyExpected.length; i++) { int iFileByte = oBIS.read(); oBAOS.write(iFileByte); int iExpectedByte = oBAIS.read(); if (iFileByte != iExpectedByte) { throw new Exception("File contents [" + ID3Util.convertBytesToHexString(oBAOS.toByteArray(), true) + "] do not match expected bytes [" + ID3Util.convertBytesToHexString(abyExpected, true) + "]."); } } } finally { oBIS.close(); } } }