/* * Created on 26-Nov-2003 * * Copyright (C)2003,2004 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: ID3V2Frame.java,v 1.24 2005/05/11 03:22:19 paul Exp $ */ package org.blinkenlights.jid3.v2; import java.io.*; import java.lang.reflect.*; import java.util.*; import java.util.zip.*; import org.blinkenlights.jid3.*; import org.blinkenlights.jid3.crypt.*; import org.blinkenlights.jid3.io.*; import org.blinkenlights.jid3.util.*; /** * @author paul * * The base class for all ID3 V2 frames. */ abstract public class ID3V2Frame implements ID3Subject, ID3Visitable { // observers to changes in this object private Set m_oID3ObserverSet = new HashSet(); // flags private boolean m_bTagAlterPreservationFlag = false; private boolean m_bFileAlterPreservationFlag = false; private boolean m_bReadOnlyFlag = false; private boolean m_bCompressionFlag = false; private boolean m_bEncryptionFlag = false; private boolean m_bGroupingIdentityFlag = false; // encryption support private byte m_byEncryptionMethod; private ICryptoAgent m_oCryptoAgent = null; private byte[] m_abyEncryptionData = null; public ID3V2Frame() { } public void addID3Observer(ID3Observer oID3Observer) { m_oID3ObserverSet.add(oID3Observer); } public void removeID3Observer(ID3Observer oID3Observer) { m_oID3ObserverSet.remove(oID3Observer); } public void notifyID3Observers() throws ID3Exception { Iterator oIter = m_oID3ObserverSet.iterator(); while (oIter.hasNext()) { ID3Observer oID3Observer = (ID3Observer)oIter.next(); oID3Observer.update(this); } } /** Specify what should happen to this frame, if the tag it is in is modified by another program * which does not recognize it. If set to true, then this frame should be discarded by a program * which does not recognize it. If set to false, it should be left as is. * * @param bTagAlterPreservationFlagValue the state this flag should be set to */ public void setTagAlterPreservationFlag(boolean bTagAlterPreservationFlagValue) { m_bTagAlterPreservationFlag = bTagAlterPreservationFlagValue; } /** Specify what should happen to this frame, if this file (other than the tag) is modified by a * program which does not recognize it. If set to true, then this frame should be discarded. If * set to false, then the frame should be preserved as is. * * @param bFileAlterPreservationFlagValue the state this flag should be set to */ public void setFileAlterPreservationFlag(boolean bFileAlterPreservationFlagValue) { m_bFileAlterPreservationFlag = bFileAlterPreservationFlagValue; } /** Specify whether this frame should be considered read only or not. * * @param bReadOnlyFlagValue the state this flag should be set to */ public void setReadOnlyFlag(boolean bReadOnlyFlagValue) { m_bReadOnlyFlag = bReadOnlyFlagValue; } /** Specify whether this frame is compressed or not. The compression method used is zlib. * * @param bCompressionFlagValue the state this flag should be set to */ public void setCompressionFlag(boolean bCompressionFlagValue) { m_bCompressionFlag = bCompressionFlagValue; } /** Set encryption method for this frame. When an encryption method is specified, the * frame will be encrypted before being written to a file. * * @param byEncryptionMethod the encryption method value to use (this value must match a * method specified in an ENCR frame in this tag) */ public void setEncryption(byte byEncryptionMethod) throws ID3Exception { m_bEncryptionFlag = true; m_byEncryptionMethod = byEncryptionMethod; notifyID3Observers(); } /** Check whether this frame is encrypted. * * @return true if the frame is encrypted, false otherwise */ public boolean isEncrypted() { return m_bEncryptionFlag; } /** Get the encryption method symbol used to encrypt this frame. * * @return the encryption method symbol used * @throws ID3Exception if this frame is not encrypted */ public byte getEncryptionMethod() throws ID3Exception { if (! isEncrypted()) { throw new ID3Exception("This frame is not encrypted."); } return m_byEncryptionMethod; } /** Set the crypto agent to be used in this frame. * * @param oCryptoAgent the crypto agent to be used for encrypting this frame * @param abyEncryptionData the encryption data to be used when encrypting/decrypting with this agent */ void setCryptoAgent(ICryptoAgent oCryptoAgent, byte[] abyEncryptionData) { m_oCryptoAgent = oCryptoAgent; m_abyEncryptionData = abyEncryptionData; } /** Specify whether this frame belongs to a group of other frames or not. If set, the group * identifier must be specified. * * @param bGroupingIdentityFlagValue the state this flag should be set to */ public void setGroupingIdentityFlag(boolean bGroupingIdentityFlagValue) { m_bGroupingIdentityFlag = bGroupingIdentityFlagValue; } /** Get the four bytes which uniquely specify of which type this frame is. */ abstract protected byte[] getFrameId(); /** Return number of bytes required to store the body of this frame. * * @return the number of bytes */ private int getLength() throws IOException { ByteArrayOutputStream oBAOS = new ByteArrayOutputStream(); ID3DataOutputStream oLengthIDOS = new ID3DataOutputStream(oBAOS); writeBody(oLengthIDOS); return oBAOS.size(); } /** Represent the contents of this frame as a string. For debugging purposes. * * @return a string representing this frame */ public abstract String toString(); /** Read an ID3 v2 frame from an ID3DataInputStream. * * @param oID3DIS input stream from which a frame can directly be read * @return an ID3V2Frame which was read from the input stream * @throws ID3Exception if an error while reading occurs */ static ID3V2Frame read(ID3DataInputStream oID3DIS) throws ID3Exception { return read(oID3DIS, new ENCRID3V2Frame[0]); } /** Read an ID3 v2 frame from an ID3DataInputStream, providing the possibility for decryption. * * @param oID3DIS input stream from which a frame can directly be read * @param aoENCRID3V2Frame the array of ENCR frames which were read in, which describe encryption details * @return an ID3V2Frame which was read from the input stream * @throws ID3Exception if an error while reading occurs */ static ID3V2Frame read(ID3DataInputStream oID3DIS, ENCRID3V2Frame[] aoENCRID3V2Frame) throws ID3Exception { String sFrameId = null; try { // read frame id byte[] abyFrameId = new byte[4]; oID3DIS.readFully(abyFrameId); if (abyFrameId[0] == 0) // we're reading into the padding past the frames { return null; } sFrameId = new String(abyFrameId); //HACK: This is a work-around for a bug in the MP3ext Windows explorer extension. It repeatedly // writes the string "MP3ext V3.3.18(unicode)" into the padding area after the frames in a v2 tag. // This is invalid, and the resulting frames are corrupt, according to the ID3 specification, which // requires that padding contain only nulls. if (sFrameId.equals("MP3e")) { return null; } if (( ! sFrameId.matches("[A-Z0-9]+")) && ID3V2Tag.usingStrict()) { throw new InvalidFrameID3Exception("Invalid frame id [" + sFrameId + "]."); } // read size int iFrameSize = oID3DIS.readBE32(); // read first flags byte int iFirstFlags = oID3DIS.readUnsignedByte(); boolean bTagAlterPreservationFlag = ((iFirstFlags & 0x80) != 0); boolean bFileAlterPreservationFlag = ((iFirstFlags & 0x40) != 0); boolean bReadOnlyFlag = ((iFirstFlags & 0x20) != 0); boolean bUnknownFirstByteFlags = ((iFirstFlags & 0x1f) != 0); // read second flags byte int iSecondFlags = oID3DIS.readUnsignedByte(); boolean bCompressionFlag = ((iSecondFlags & 0x80) != 0); boolean bEncryptionFlag = ((iSecondFlags & 0x40) != 0); boolean bGroupingIdentityFlag = ((iSecondFlags & 0x20) != 0); boolean bUnknownSecondByteFlags = ((iSecondFlags & 0x1f) != 0); // get length of uncompressed frame if compression set int iUncompressedSize = iFrameSize; if (bCompressionFlag) { iUncompressedSize = oID3DIS.readBE32(); iFrameSize -= 4; // FIX: four bytes read for frame size } // read encryption method byte, if used int iEncryptionMethodSymbol = 0; ICryptoAgent oCryptoAgent = null; byte[] abyEncryptionData = null; if (bEncryptionFlag) { iEncryptionMethodSymbol = oID3DIS.readUnsignedByte(); iFrameSize -= 1; // FIX: one byte for encryption method // this frame is encrypted.. do we have a means of decrypting it? for (int i=0; i < aoENCRID3V2Frame.length; i++) { if ((aoENCRID3V2Frame[i].getEncryptionMethodSymbol() & 0xff) == iEncryptionMethodSymbol) { // we can decrypt this frame now oCryptoAgent = ID3Encryption.getInstance().lookupCryptoAgent(aoENCRID3V2Frame[i].getOwnerIdentifier()); abyEncryptionData = aoENCRID3V2Frame[i].getEncryptionData(); break; } } if (oCryptoAgent == null) { ByteArrayOutputStream oEncryptedBAOS = new ByteArrayOutputStream(); ID3DataOutputStream oEncryptedIDOS = new ID3DataOutputStream(oEncryptedBAOS); oEncryptedIDOS.write(abyFrameId); oEncryptedIDOS.writeBE32(iFrameSize + (bEncryptionFlag ? 1 : 0)); oEncryptedIDOS.writeUnsignedByte(iFirstFlags); oEncryptedIDOS.writeUnsignedByte(iSecondFlags); if (bCompressionFlag) { oEncryptedIDOS.writeID3Four(iUncompressedSize); } oEncryptedIDOS.writeUnsignedByte(iEncryptionMethodSymbol); // determine the length of the compressed/encrypted data to be read in (minus the after header bytes we've already read) int iFrameDataLength = iFrameSize; // initial length of frame data before data we have already read // read compressed/encrypted data byte[] abyEncryptedFrameData = new byte[iFrameDataLength]; oID3DIS.readFully(abyEncryptedFrameData); oEncryptedIDOS.write(abyEncryptedFrameData); // we cannot decrypt this frame at this time, so return it, as we read it, as a special encrypted frame object EncryptedID3V2Frame oEncryptedFrame = new EncryptedID3V2Frame(sFrameId, oEncryptedBAOS.toByteArray()); return oEncryptedFrame; } } // read frame data byte[] abyFrameData = null; if (bCompressionFlag) { // read compressed data byte[] abyCompressedFrameData = new byte[iFrameSize]; oID3DIS.readFully(abyCompressedFrameData); // decrypt compressed data first, if encrypted if (bEncryptionFlag) { abyCompressedFrameData = oCryptoAgent.decrypt(abyCompressedFrameData, abyEncryptionData); } // deflate data ByteArrayInputStream oBAIS = new ByteArrayInputStream(abyCompressedFrameData); InflaterInputStream oInflaterIS = new InflaterInputStream(oBAIS); ID3DataInputStream oInflaterID3DIS = new ID3DataInputStream(oInflaterIS); abyFrameData = new byte[iUncompressedSize]; oInflaterID3DIS.readFully(abyFrameData); } else { abyFrameData = new byte[iFrameSize]; oID3DIS.readFully(abyFrameData); // decrypt data, if encrypted if (bEncryptionFlag) { abyFrameData = oCryptoAgent.decrypt(abyFrameData, abyEncryptionData); } } // create a frame object here based on what we've read ID3V2Frame oID3V2Frame; if (sFrameId.startsWith("T")) { // text information frame String sClassName = "org.blinkenlights.jid3.v2." + sFrameId + "TextInformationID3V2Frame"; // if this class exists, then create such an object try { Class oID3V2FrameClass = Class.forName(sClassName); Class[] aoArgClassTypes = { InputStream.class }; Constructor oConstructor = oID3V2FrameClass.getConstructor(aoArgClassTypes); Object[] aoConstructorArgs = { new ByteArrayInputStream(abyFrameData) }; oID3V2Frame = (ID3V2Frame)oConstructor.newInstance(aoConstructorArgs); } catch (ClassNotFoundException e) { // unknown frame type oID3V2Frame = new UnknownTextInformationID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData)); } catch (NoSuchMethodException e) { // unknown frame type oID3V2Frame = new UnknownTextInformationID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData)); } catch (InvocationTargetException e) { // constructor threw an exception if (e.getCause() instanceof Exception) { throw (Exception)e.getCause(); } else { throw e; } } } else if (sFrameId.startsWith("W")) { // URL link frame String sClassName = "org.blinkenlights.jid3.v2." + sFrameId + "UrlLinkID3V2Frame"; // if this class exists, then create such an object try { Class oID3V2FrameClass = Class.forName(sClassName); Class[] aoArgClassTypes = { InputStream.class }; Constructor oConstructor = oID3V2FrameClass.getConstructor(aoArgClassTypes); Object[] aoConstructorArgs = { new ByteArrayInputStream(abyFrameData) }; oID3V2Frame = (ID3V2Frame)oConstructor.newInstance(aoConstructorArgs); } catch (ClassNotFoundException e) { // unknown frame type oID3V2Frame = new UnknownUrlLinkID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData)); } catch (NoSuchMethodException e) { // unknown frame type oID3V2Frame = new UnknownUrlLinkID3V2Frame(sFrameId, new ByteArrayInputStream(abyFrameData)); } catch (InvocationTargetException e) { // constructor threw an exception if (e.getCause() instanceof Exception) { throw (Exception)e.getCause(); } else { throw e; } } } else { // unique frame String sClassName = "org.blinkenlights.jid3.v2." + sFrameId + "ID3V2Frame"; // if this class exists, then create such an object try { Class oID3V2FrameClass = Class.forName(sClassName); Class[] aoArgClassTypes = { InputStream.class }; Constructor oConstructor = oID3V2FrameClass.getConstructor(aoArgClassTypes); Object[] aoConstructorArgs = { new ByteArrayInputStream(abyFrameData) }; oID3V2Frame = (ID3V2Frame)oConstructor.newInstance(aoConstructorArgs); } catch (ClassNotFoundException e) { // unknown frame oID3V2Frame = new UnknownID3V2Frame(sFrameId, abyFrameData); } catch (NoSuchMethodException e) { // unknown frame type oID3V2Frame = new UnknownID3V2Frame(sFrameId, abyFrameData); } } // set flags applicable to all v2 frames oID3V2Frame.setTagAlterPreservationFlag(bTagAlterPreservationFlag); oID3V2Frame.setFileAlterPreservationFlag(bFileAlterPreservationFlag); oID3V2Frame.setReadOnlyFlag(bReadOnlyFlag); oID3V2Frame.setCompressionFlag(bCompressionFlag); if (bEncryptionFlag) { oID3V2Frame.setEncryption((byte)iEncryptionMethodSymbol); } oID3V2Frame.setGroupingIdentityFlag(bGroupingIdentityFlag); return oID3V2Frame; } catch (ID3Exception e) { throw e; } catch (Exception e) { if (sFrameId == null) { throw new ID3Exception("Error reading v2 frame.", e); } else { throw new ID3Exception("Error reading " + sFrameId + " v2 frame.", e); } } } /** Write the header of this frame to an output stream. * * @param oOS the output stream to write to * @throws ID3Exception if an error occurs while writing */ protected void writeHeader(OutputStream oOS) throws ID3Exception { try { ID3DataOutputStream oIDOS = new ID3DataOutputStream(oOS); // frame id oIDOS.write(getFrameId()); // size int iActualLength = getActualLength(); oIDOS.writeBE32(iActualLength); //oIDOS.writeBE32(getLength()); // first flags int iFirstFlags = 0; if (m_bTagAlterPreservationFlag) { iFirstFlags |= (1 << 7); } if (m_bFileAlterPreservationFlag) { iFirstFlags |= (1 << 6); } if (m_bReadOnlyFlag) { iFirstFlags |= (1 << 5); } oIDOS.writeUnsignedByte(iFirstFlags); // second flags int iSecondFlags = 0; if (m_bCompressionFlag) { iSecondFlags |= (1 << 7); } if (m_bEncryptionFlag) { iSecondFlags |= (1 << 6); } if (m_bGroupingIdentityFlag) { iSecondFlags |= (1 << 5); } oIDOS.writeUnsignedByte(iSecondFlags); // write uncompressed length of the body, if it is compressed if (m_bCompressionFlag) { oIDOS.writeBE32(getLength()); } // write encrypted method, if used if (m_bEncryptionFlag) { oIDOS.writeUnsignedByte(m_byEncryptionMethod & 0xff); } } catch (Exception e) { throw new ID3Exception("Error writing frame: " + e.getMessage(), e); } } /** Returns the length in bytes that the body of the frame will require when actually written to * a file. This may be shorter than the default length, if the frame is compressed. * * @return the length of the frame when written * @throws IOException if an error occurs while determining the compressed length */ private int getActualLength() throws Exception { ByteArrayOutputStream oBAOS = new ByteArrayOutputStream(); ID3DataOutputStream oIDOS = new ID3DataOutputStream(oBAOS); writeBody(oIDOS); byte[] abyBody = oBAOS.toByteArray(); if (m_bCompressionFlag) { ByteArrayOutputStream oCompressedBAOS = new ByteArrayOutputStream(); DeflaterOutputStream oDeflaterOS = new DeflaterOutputStream(oCompressedBAOS); oDeflaterOS.write(abyBody); oDeflaterOS.finish(); abyBody = oCompressedBAOS.toByteArray(); } if (m_bEncryptionFlag) { if (m_oCryptoAgent == null) { throw new ID3Exception("Crypto agent for method " + m_byEncryptionMethod + " not registered. Cannot write frame."); } abyBody = m_oCryptoAgent.encrypt(abyBody, m_abyEncryptionData); } return abyBody.length + (m_bCompressionFlag ? 4 : 0) + (m_bEncryptionFlag ? 1 : 0); } /** Write the body of the frame to an ID3 data output stream. * * @param oIDOS the output stream to write to * @throws ID3Exception if an error occurs while writing */ protected abstract void writeBody(ID3DataOutputStream oIDOS) throws IOException; /** Write this frame to an output stream. * * @param oOS the output stream to write to * @throws ID3Exception if an error occurs while writing the frame * @throws IOException if an error occurs while writing the frame */ public void write(OutputStream oOS) throws IOException, ID3Exception { ID3DataOutputStream oIDOS = new ID3DataOutputStream(oOS); // write header writeHeader(oIDOS); // write body byte[] abyBody = null; // put original body bytes in abyBody ByteArrayOutputStream oBodyBAOS = new ByteArrayOutputStream(); ID3DataOutputStream oBodyIDOS = new ID3DataOutputStream(oBodyBAOS); writeBody(oBodyIDOS); abyBody = oBodyBAOS.toByteArray(); // if compression used, compress body byte array if (m_bCompressionFlag) { ByteArrayOutputStream oCompressedBAOS = new ByteArrayOutputStream(); DeflaterOutputStream oDeflaterOS = new DeflaterOutputStream(oCompressedBAOS); oDeflaterOS.write(abyBody); oDeflaterOS.finish(); abyBody = oCompressedBAOS.toByteArray(); } // if encryption used, encrypt body byte array if (m_bEncryptionFlag) { if (m_oCryptoAgent == null) { throw new ID3Exception("Crypto agent for method " + m_byEncryptionMethod + " not registered. Cannot write frame."); } abyBody = m_oCryptoAgent.encrypt(abyBody, m_abyEncryptionData); } oIDOS.write(abyBody); } }