Rewrite chapter parser for testability

This commit is contained in:
ByteHamster 2021-02-16 12:28:58 +01:00
parent 0f692be2d4
commit 3f1c1c4bf5
7 changed files with 428 additions and 335 deletions

View File

@ -95,14 +95,9 @@ public class ChapterUtils {
@NonNull @NonNull
private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException { private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
ChapterReader reader = new ChapterReader(); ChapterReader reader = new ChapterReader(in);
reader.readInputStream(in); reader.readInputStream();
List<Chapter> chapters = reader.getChapters(); List<Chapter> chapters = reader.getChapters();
if (chapters == null) {
Log.i(TAG, "ChapterReader could not find any ID3 chapters");
return Collections.emptyList();
}
Collections.sort(chapters, new ChapterStartTimeComparator()); Collections.sort(chapters, new ChapterStartTimeComparator());
enumerateEmptyChapterTitles(chapters); enumerateEmptyChapterTitles(chapters);
if (!chaptersValid(chapters)) { if (!chaptersValid(chapters)) {
@ -117,10 +112,6 @@ public class ChapterUtils {
VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
reader.readInputStream(input); reader.readInputStream(input);
List<Chapter> chapters = reader.getChapters(); List<Chapter> chapters = reader.getChapters();
if (chapters == null) {
Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters");
return Collections.emptyList();
}
Collections.sort(chapters, new ChapterStartTimeComparator()); Collections.sort(chapters, new ChapterStartTimeComparator());
enumerateEmptyChapterTitles(chapters); enumerateEmptyChapterTitles(chapters);
if (chaptersValid(chapters)) { if (chaptersValid(chapters)) {

View File

@ -2,149 +2,114 @@ package de.danoeh.antennapod.core.util.id3reader;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.ID3Chapter; import de.danoeh.antennapod.core.feed.ID3Chapter;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; import org.apache.commons.io.input.CountingInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.commons.io.input.CountingInputStream;
/**
* Reads ID3 chapters.
* See https://id3.org/id3v2-chapters-1.0
*/
public class ChapterReader extends ID3Reader { public class ChapterReader extends ID3Reader {
private static final String TAG = "ID3ChapterReader"; private static final String TAG = "ID3ChapterReader";
private static final String FRAME_ID_CHAPTER = "CHAP"; public static final String FRAME_ID_CHAPTER = "CHAP";
private static final String FRAME_ID_TITLE = "TIT2"; public static final String FRAME_ID_TITLE = "TIT2";
private static final String FRAME_ID_LINK = "WXXX"; public static final String FRAME_ID_LINK = "WXXX";
private static final String FRAME_ID_PICTURE = "APIC"; public static final String FRAME_ID_PICTURE = "APIC";
private static final int IMAGE_TYPE_COVER = 3; public static final String MIME_IMAGE_URL = "-->";
public static final int IMAGE_TYPE_COVER = 3;
private List<Chapter> chapters; private final List<Chapter> chapters = new ArrayList<>();
private ID3Chapter currentChapter;
@Override public ChapterReader(CountingInputStream input) {
public int onStartTagHeader(TagHeader header) { super(input);
chapters = new ArrayList<>();
Log.d(TAG, "header: " + header);
return ID3Reader.ACTION_DONT_SKIP;
} }
@Override @Override
public int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException { protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
Log.d(TAG, "header: " + header); if (FRAME_ID_CHAPTER.equals(frameHeader.getId())) {
switch (header.getId()) { Log.d(TAG, "Handling frame: " + frameHeader.toString());
case FRAME_ID_CHAPTER: Chapter chapter = readChapter(frameHeader);
if (currentChapter != null) { Log.d(TAG, "Chapter done: " + chapter);
if (!hasId3Chapter(currentChapter)) { chapters.add(chapter);
chapters.add(currentChapter); } else {
Log.d(TAG, "Found chapter: " + currentChapter); super.readFrame(frameHeader);
currentChapter = null;
} }
} }
StringBuilder elementId = new StringBuilder();
readISOString(elementId, input, Integer.MAX_VALUE); public Chapter readChapter(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
char[] startTimeSource = readChars(input, 4); int chapterStartedPosition = getPosition();
long startTime = ((int) startTimeSource[0] << 24) String elementId = readIsoStringNullTerminated(100);
| ((int) startTimeSource[1] << 16) long startTime = readInt();
| ((int) startTimeSource[2] << 8) | startTimeSource[3]; skipBytes(12); // Ignore end time, start offset, end offset
currentChapter = new ID3Chapter(elementId.toString(), startTime); ID3Chapter chapter = new ID3Chapter(elementId, startTime);
skipBytes(input, 12);
return ID3Reader.ACTION_DONT_SKIP; // Let reader discover the sub-frames // Read sub-frames
while (getPosition() < chapterStartedPosition + frameHeader.getSize()) {
FrameHeader subFrameHeader = readFrameHeader();
readChapterSubFrame(subFrameHeader, chapter);
}
return chapter;
}
public void readChapterSubFrame(@NonNull FrameHeader frameHeader, @NonNull Chapter chapter)
throws IOException, ID3ReaderException {
Log.d(TAG, "Handling subframe: " + frameHeader.toString());
int frameStartPosition = getPosition();
switch (frameHeader.getId()) {
case FRAME_ID_TITLE: case FRAME_ID_TITLE:
if (currentChapter != null && currentChapter.getTitle() == null) { chapter.setTitle(readEncodingAndString(frameHeader.getSize()));
StringBuilder title = new StringBuilder(); Log.d(TAG, "Found title: " + chapter.getTitle());
readString(title, input, header.getSize());
currentChapter
.setTitle(title.toString());
Log.d(TAG, "Found title: " + currentChapter.getTitle());
return ID3Reader.ACTION_SKIP;
}
break; break;
case FRAME_ID_LINK: case FRAME_ID_LINK:
if (currentChapter != null) { readEncodingAndString(frameHeader.getSize()); // skip description
// skip description String url = readIsoStringNullTerminated(frameStartPosition + frameHeader.getSize() - getPosition());
int descriptionLength = readString(null, input, header.getSize());
StringBuilder link = new StringBuilder();
readISOString(link, input, header.getSize() - descriptionLength);
try { try {
String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); String decodedLink = URLDecoder.decode(url, "ISO-8859-1");
currentChapter.setLink(decodedLink); chapter.setLink(decodedLink);
Log.d(TAG, "Found link: " + currentChapter.getLink()); Log.d(TAG, "Found link: " + chapter.getLink());
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
Log.w(TAG, "Bad URL found in ID3 data"); Log.w(TAG, "Bad URL found in ID3 data");
} }
return ID3Reader.ACTION_SKIP;
}
break; break;
case FRAME_ID_PICTURE: case FRAME_ID_PICTURE:
if (currentChapter != null) { byte encoding = readByte();
Log.d(TAG, header.toString()); String mime = readEncodedString(encoding, frameHeader.getSize());
StringBuilder mime = new StringBuilder(); byte type = readByte();
int read = readString(mime, input, header.getSize()); String description = readEncodedString(encoding, frameHeader.getSize());
byte type = (byte) readChars(input, 1)[0];
read++;
StringBuilder description = new StringBuilder();
read += readISOString(description, input, header.getSize()); // Should use same encoding as mime
Log.d(TAG, "Found apic: " + mime + "," + description); Log.d(TAG, "Found apic: " + mime + "," + description);
if (mime.toString().equals("-->")) { if (MIME_IMAGE_URL.equals(mime)) {
// Data contains a link to a picture String link = readIsoStringNullTerminated(frameHeader.getSize());
StringBuilder link = new StringBuilder(); Log.d(TAG, "Link: " + link);
readISOString(link, input, header.getSize()); if (TextUtils.isEmpty(chapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
Log.d(TAG, "link: " + link.toString()); chapter.setImageUrl(link);
if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
currentChapter.setImageUrl(link.toString());
} }
} else { } else {
// Data contains the picture int alreadyConsumed = getPosition() - frameStartPosition;
int length = header.getSize() - read; int rawImageDataLength = frameHeader.getSize() - alreadyConsumed;
if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) { if (TextUtils.isEmpty(chapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
currentChapter.setImageUrl(EmbeddedChapterImage.makeUrl(input.getCount(), length)); chapter.setImageUrl(EmbeddedChapterImage.makeUrl(getPosition(), rawImageDataLength));
} }
skipBytes(input, length);
}
return ID3Reader.ACTION_SKIP;
} }
break; break;
default:
Log.d(TAG, "Unknown chapter sub-frame.");
break;
} }
return super.onStartFrameHeader(header, input); // Skip garbage to fill frame completely
} // This also asserts that we are not reading too many bytes from this frame.
int alreadyConsumed = getPosition() - frameStartPosition;
private boolean hasId3Chapter(ID3Chapter chapter) { skipBytes(frameHeader.getSize() - alreadyConsumed);
for (Chapter c : chapters) {
if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) {
return true;
}
}
return false;
}
@Override
public void onEndTag() {
if (currentChapter != null) {
if (!hasId3Chapter(currentChapter)) {
chapters.add(currentChapter);
}
}
Log.d(TAG, "Reached end of tag");
if (chapters != null) {
for (Chapter c : chapters) {
Log.d(TAG, "chapter: " + c);
}
}
}
@Override
public void onNoTagHeaderFound() {
Log.d(TAG, "No tag header found");
super.onNoTagHeaderFound();
} }
public List<Chapter> getChapters() { public List<Chapter> getChapters() {

View File

@ -1,163 +1,112 @@
package de.danoeh.antennapod.core.util.id3reader; package de.danoeh.antennapod.core.util.id3reader;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CountingInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
import org.apache.commons.io.input.CountingInputStream;
/** /**
* Reads the ID3 Tag of a given file. In order to use this class, you should * Reads the ID3 Tag of a given file.
* create a subclass of it and overwrite the onStart* - or onEnd* - methods. * See https://id3.org/id3v2.3.0
*/ */
public class ID3Reader { public class ID3Reader {
private static final int HEADER_LENGTH = 10; private static final String TAG = "ID3Reader";
private static final int ID3_LENGTH = 3;
private static final int FRAME_ID_LENGTH = 4; private static final int FRAME_ID_LENGTH = 4;
public static final byte ENCODING_ISO = 0;
/** public static final byte ENCODING_UTF16_WITH_BOM = 1;
* Should skip remaining bytes of the current frame. public static final byte ENCODING_UTF16_WITHOUT_BOM = 2;
*/ public static final byte ENCODING_UTF8 = 3;
static final int ACTION_SKIP = 1;
/**
* Should not skip remaining bytes of the current frame. Can be used to parse sub-frames.
*/
static final int ACTION_DONT_SKIP = 2;
private int readerPosition;
private static final byte ENCODING_UTF16_WITH_BOM = 1;
private static final byte ENCODING_UTF16_WITHOUT_BOM = 2;
private static final byte ENCODING_UTF8 = 3;
private TagHeader tagHeader; private TagHeader tagHeader;
private final CountingInputStream inputStream;
ID3Reader() { public ID3Reader(CountingInputStream input) {
inputStream = input;
} }
public final void readInputStream(CountingInputStream input) throws IOException, ID3ReaderException { public void readInputStream() throws IOException, ID3ReaderException {
int rc; tagHeader = readTagHeader();
readerPosition = 0; int tagContentStartPosition = getPosition();
char[] tagHeaderSource = readChars(input, HEADER_LENGTH); while (getPosition() < tagContentStartPosition + tagHeader.getSize()) {
tagHeader = createTagHeader(tagHeaderSource); FrameHeader frameHeader = readFrameHeader();
if (tagHeader == null) { if (frameHeader.getId().charAt(0) < '0' || frameHeader.getId().charAt(0) > 'z') {
onNoTagHeaderFound(); Log.d(TAG, "Stopping because of invalid frame: " + frameHeader.toString());
} else { return;
rc = onStartTagHeader(tagHeader);
if (rc != ACTION_SKIP) {
while (readerPosition < tagHeader.getSize()) {
FrameHeader frameHeader = createFrameHeader(readChars(input, HEADER_LENGTH));
if (checkForNullString(frameHeader.getId())) {
break;
} }
int readerPositionBeforeFrame = input.getCount(); readFrame(frameHeader);
rc = onStartFrameHeader(frameHeader, input);
if (rc == ACTION_SKIP) {
if (frameHeader.getSize() + readerPosition > tagHeader.getSize()) {
break;
}
int bytesAlreadyHandled = input.getCount() - readerPositionBeforeFrame;
int bytesLeftToSkip = frameHeader.getSize() - bytesAlreadyHandled;
if (bytesLeftToSkip > 0) {
skipBytes(input, bytesLeftToSkip);
}
}
}
}
onEndTag();
} }
} }
/** Returns true if string only contains null-bytes. */ protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
private boolean checkForNullString(String s) { Log.d(TAG, "Skipping frame: " + frameHeader.toString());
if (!s.isEmpty()) { skipBytes(frameHeader.getSize());
int i = 0;
if (s.charAt(i) == 0) {
for (i = 1; i < s.length(); i++) {
if (s.charAt(i) != 0) {
return false;
}
}
return true;
}
return false;
} else {
return true;
} }
int getPosition() {
return inputStream.getCount();
} }
/** /**
* Read a certain number of chars from the given input stream. This method * Skip a certain number of bytes on the given input stream.
* changes the readerPosition-attribute.
*/ */
char[] readChars(InputStream input, int number) throws IOException, ID3ReaderException { void skipBytes(int number) throws IOException, ID3ReaderException {
char[] header = new char[number]; if (number < 0) {
for (int i = 0; i < number; i++) { throw new ID3ReaderException("Trying to read a negative number of bytes");
int b = input.read();
readerPosition++;
if (b != -1) {
header[i] = (char) b;
} else {
throw new ID3ReaderException("Unexpected end of stream");
} }
} IOUtils.skipFully(inputStream, number);
return header;
} }
/** byte readByte() throws IOException {
* Skip a certain number of bytes on the given input stream. This method return (byte) inputStream.read();
* changes the readerPosition-attribute.
*/
void skipBytes(InputStream input, int number) throws IOException {
if (number <= 0) {
number = 1;
}
IOUtils.skipFully(input, number);
readerPosition += number;
} }
private TagHeader createTagHeader(char[] source) throws ID3ReaderException { short readShort() throws IOException {
boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44) char firstByte = (char) inputStream.read();
&& (source[2] == 0x33); char secondByte = (char) inputStream.read();
if (source.length != HEADER_LENGTH) { return (short) ((firstByte << 8) | secondByte);
throw new ID3ReaderException("Length of header must be "
+ HEADER_LENGTH);
} }
if (hasTag) {
String id = new String(source, 0, ID3_LENGTH); int readInt() throws IOException {
char version = (char) ((source[3] << 8) | source[4]); char firstByte = (char) inputStream.read();
byte flags = (byte) source[5]; char secondByte = (char) inputStream.read();
int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8) char thirdByte = (char) inputStream.read();
| source[9]; char fourthByte = (char) inputStream.read();
size = unsynchsafe(size); return (firstByte << 24) | (secondByte << 16) | (thirdByte << 8) | fourthByte;
return new TagHeader(id, size, version, flags); }
} else {
return null; void expectChar(char expected) throws ID3ReaderException, IOException {
char read = (char) inputStream.read();
if (read != expected) {
throw new ID3ReaderException("Expected " + expected + " and got " + read);
} }
} }
private FrameHeader createFrameHeader(char[] source) @NonNull
throws ID3ReaderException { TagHeader readTagHeader() throws ID3ReaderException, IOException {
if (source.length != HEADER_LENGTH) { expectChar('I');
throw new ID3ReaderException("Length of header must be " expectChar('D');
+ HEADER_LENGTH); expectChar('3');
short version = readShort();
byte flags = readByte();
int size = unsynchsafe(readInt());
return new TagHeader("ID3", size, version, flags);
} }
String id = new String(source, 0, FRAME_ID_LENGTH);
int size = (((int) source[4]) << 24) | (((int) source[5]) << 16) @NonNull
| (((int) source[6]) << 8) | source[7]; FrameHeader readFrameHeader() throws IOException {
String id = readIsoStringFixed(FRAME_ID_LENGTH);
int size = readInt();
if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { if (tagHeader != null && tagHeader.getVersion() >= 0x0400) {
size = unsynchsafe(size); size = unsynchsafe(size);
} }
char flags = (char) ((source[8] << 8) | source[9]); short flags = readShort();
return new FrameHeader(id, size, flags); return new FrameHeader(id, size, flags);
} }
@ -174,81 +123,74 @@ public class ID3Reader {
return out; return out;
} }
protected int readString(StringBuilder buffer, InputStream input, int max) throws IOException, /**
ID3ReaderException { * Reads a null-terminated string with encoding.
if (max > 0) { */
char[] encoding = readChars(input, 1); protected String readEncodingAndString(int max) throws IOException {
max--; byte encoding = readByte();
return readEncodedString(encoding, max - 1);
if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) {
return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account
} else if (encoding[0] == ENCODING_UTF8) {
return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account
} else {
return readISOString(buffer, input, max) + 1; // take encoding byte into account
}
} else {
if (buffer != null) {
buffer.append("");
}
return 0;
}
} }
protected int readISOString(StringBuilder buffer, InputStream input, int max) @SuppressWarnings("CharsetObjectCanBeUsed")
throws IOException, ID3ReaderException { protected String readIsoStringFixed(int length) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int bytesRead = 0; int bytesRead = 0;
char c; while (bytesRead < length) {
while (++bytesRead <= max && (c = (char) input.read()) > 0) { bytes.write(readByte());
if (buffer != null) { bytesRead++;
buffer.append(c);
} }
} return Charset.forName("ISO-8859-1").newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
return bytesRead;
} }
private int readUnicodeString(StringBuilder strBuffer, InputStream input, int max, Charset charset) protected String readIsoStringNullTerminated(int max) throws IOException {
throws IOException, ID3ReaderException { return readEncodedString(ENCODING_ISO, max);
byte[] buffer = new byte[max]; }
int c;
int cZero = -1; @SuppressWarnings("CharsetObjectCanBeUsed")
int i = 0; String readEncodedString(int encoding, int max) throws IOException {
for (; i < max; i++) { if (encoding == ENCODING_UTF16_WITH_BOM || encoding == ENCODING_UTF16_WITHOUT_BOM) {
c = input.read(); return readEncodedString2(Charset.forName("UTF-16"), max);
if (c == -1) { } else if (encoding == ENCODING_UTF8) {
break; return readEncodedString2(Charset.forName("UTF-8"), max);
} else if (c == 0) {
if (cZero == 0) {
// termination character found
break;
} else { } else {
cZero = 0; return readEncodedString1(Charset.forName("ISO-8859-1"), max);
} }
} else {
buffer[i] = (byte) c;
cZero = -1;
}
}
if (strBuffer != null) {
strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString());
}
return i;
} }
int onStartTagHeader(TagHeader header) { /**
return ACTION_SKIP; * Reads chars where the encoding uses 1 char per symbol.
*/
private String readEncodedString1(Charset charset, int max) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int bytesRead = 0;
while (bytesRead < max) {
byte c = readByte();
bytesRead++;
if (c == 0) {
break;
}
bytes.write(c);
}
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
} }
int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException { /**
return ACTION_SKIP; * Reads chars where the encoding uses 2 chars per symbol.
*/
private String readEncodedString2(Charset charset, int max) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int bytesRead = 0;
while (bytesRead < max) {
byte c1 = readByte();
bytesRead++;
if (c1 == 0) {
break;
} }
byte c2 = readByte();
void onEndTag() { bytesRead++;
bytes.write(c1);
bytes.write(c2);
} }
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
void onNoTagHeaderFound() {
} }
} }

View File

@ -3,10 +3,9 @@ package de.danoeh.antennapod.core.util.id3reader.model;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
public class FrameHeader extends Header { public class FrameHeader extends Header {
private final short flags;
private final char flags; public FrameHeader(String id, int size, short flags) {
public FrameHeader(String id, int size, char flags) {
super(id, size); super(id, size);
this.flags = flags; this.flags = flags;
} }

View File

@ -1,26 +1,25 @@
package de.danoeh.antennapod.core.util.id3reader.model; package de.danoeh.antennapod.core.util.id3reader.model;
public class TagHeader extends Header { import androidx.annotation.NonNull;
private final char version; public class TagHeader extends Header {
private final short version;
private final byte flags; private final byte flags;
public TagHeader(String id, int size, char version, byte flags) { public TagHeader(String id, int size, short version, byte flags) {
super(id, size); super(id, size);
this.version = version; this.version = version;
this.flags = flags; this.flags = flags;
} }
@Override @Override
@NonNull
public String toString() { public String toString() {
return "TagHeader [version=" + version + ", flags=" + flags + ", id=" return "TagHeader [version=" + version + ", flags=" + flags + ", id="
+ id + ", size=" + size + "]"; + id + ", size=" + size + "]";
} }
public char getVersion() { public short getVersion() {
return version; return version;
} }
} }

View File

@ -0,0 +1,105 @@
package de.danoeh.antennapod.core.util.id3reader;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.ID3Chapter;
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
import org.apache.commons.io.input.CountingInputStream;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import static de.danoeh.antennapod.core.util.id3reader.Id3ReaderTest.concat;
import static de.danoeh.antennapod.core.util.id3reader.Id3ReaderTest.generateFrameHeader;
import static de.danoeh.antennapod.core.util.id3reader.Id3ReaderTest.generateId3Header;
import static org.junit.Assert.assertEquals;
public class ChapterReaderTest {
private static final byte CHAPTER_WITHOUT_SUBFRAME_START_TIME = 23;
private static final byte[] CHAPTER_WITHOUT_SUBFRAME = {
'C', 'H', '1', 0, // String ID for mapping to CTOC
0, 0, 0, CHAPTER_WITHOUT_SUBFRAME_START_TIME, // Start time
0, 0, 0, 0, // End time
0, 0, 0, 0, // Start offset
0, 0, 0, 0 // End offset
};
@Test
public void testReadFullTagWithChapter() throws IOException, ID3ReaderException {
byte[] chapter = concat(
generateFrameHeader(ChapterReader.FRAME_ID_CHAPTER, CHAPTER_WITHOUT_SUBFRAME.length),
CHAPTER_WITHOUT_SUBFRAME);
byte[] data = concat(
generateId3Header(chapter.length),
chapter);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data));
ChapterReader reader = new ChapterReader(inputStream);
reader.readInputStream();
assertEquals(1, reader.getChapters().size());
assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, reader.getChapters().get(0).getStart());
}
@Test
public void testReadFullTagWithMultipleChapters() throws IOException, ID3ReaderException {
byte[] chapter = concat(
generateFrameHeader(ChapterReader.FRAME_ID_CHAPTER, CHAPTER_WITHOUT_SUBFRAME.length),
CHAPTER_WITHOUT_SUBFRAME);
byte[] data = concat(
generateId3Header(2 * chapter.length),
chapter,
chapter);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data));
ChapterReader reader = new ChapterReader(inputStream);
reader.readInputStream();
assertEquals(2, reader.getChapters().size());
assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, reader.getChapters().get(0).getStart());
assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, reader.getChapters().get(1).getStart());
}
@Test
public void testReadChapterWithoutSubframes() throws IOException, ID3ReaderException {
FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_CHAPTER,
CHAPTER_WITHOUT_SUBFRAME.length, (short) 0);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(CHAPTER_WITHOUT_SUBFRAME));
Chapter chapter = new ChapterReader(inputStream).readChapter(header);
assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, chapter.getStart());
}
@Test
public void testReadChapterWithTitle() throws IOException, ID3ReaderException {
byte[] title = {
ID3Reader.ENCODING_ISO,
'H', 'e', 'l', 'l', 'o', // Title
0 // Null-terminated
};
byte[] chapterData = concat(
CHAPTER_WITHOUT_SUBFRAME,
generateFrameHeader(ChapterReader.FRAME_ID_TITLE, title.length),
title);
FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_CHAPTER, chapterData.length, (short) 0);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(chapterData));
ChapterReader reader = new ChapterReader(inputStream);
Chapter chapter = reader.readChapter(header);
assertEquals(CHAPTER_WITHOUT_SUBFRAME_START_TIME, chapter.getStart());
assertEquals("Hello", chapter.getTitle());
}
@Test
public void testReadTitleWithGarbage() throws IOException, ID3ReaderException {
byte[] titleSubframeContent = {
ID3Reader.ENCODING_ISO,
'A', // Title
0, // Null-terminated
42, 42, 42, 42 // Garbage, should be ignored
};
FrameHeader header = new FrameHeader(ChapterReader.FRAME_ID_TITLE, titleSubframeContent.length, (short) 0);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(titleSubframeContent));
ChapterReader reader = new ChapterReader(inputStream);
Chapter chapter = new ID3Chapter("", 0);
reader.readChapterSubFrame(header, chapter);
assertEquals("A", chapter.getTitle());
// Should skip the garbage and point to the next frame
assertEquals(titleSubframeContent.length, reader.getPosition());
}
}

View File

@ -0,0 +1,92 @@
package de.danoeh.antennapod.core.util.id3reader;
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
import org.apache.commons.io.input.CountingInputStream;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class Id3ReaderTest {
@Test
public void testReadString() throws IOException {
byte[] data = {
ID3Reader.ENCODING_ISO,
'T', 'e', 's', 't',
0 // Null-terminated
};
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data));
String string = new ID3Reader(inputStream).readEncodingAndString(1000);
assertEquals("Test", string);
}
@Test
public void testReadUtf16WithBom() throws IOException {
byte[] data = {
ID3Reader.ENCODING_UTF16_WITH_BOM,
(byte) 0xff, (byte) 0xfe, // BOM
'A', 0, 'B', 0, 'C', 0,
0, 0, // Null-terminated
};
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data));
String string = new ID3Reader(inputStream).readEncodingAndString(1000);
assertEquals("ABC", string);
}
@Test
public void testReadTagHeader() throws IOException, ID3ReaderException {
byte[] data = generateId3Header(23);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data));
TagHeader header = new ID3Reader(inputStream).readTagHeader();
assertEquals("ID3", header.getId());
assertEquals(42, header.getVersion());
assertEquals(23, header.getSize());
}
@Test
public void testReadFrameHeader() throws IOException {
byte[] data = generateFrameHeader("CHAP", 42);
CountingInputStream inputStream = new CountingInputStream(new ByteArrayInputStream(data));
FrameHeader header = new ID3Reader(inputStream).readFrameHeader();
assertEquals("CHAP", header.getId());
assertEquals(42, header.getSize());
}
public static byte[] generateFrameHeader(String id, int size) {
return concat(
id.getBytes(StandardCharsets.ISO_8859_1), // Frame ID
new byte[] {
(byte) (size >> 24), (byte) (size >> 16),
(byte) (size >> 8), (byte) (size), // Size
0, 0 // Flags
});
}
static byte[] generateId3Header(int size) {
return new byte[] {
'I', 'D', '3', // Identifier
0, 42, // Version
0, // Flags
(byte) (size >> 24), (byte) (size >> 16),
(byte) (size >> 8), (byte) (size), // Size
};
}
static byte[] concat(byte[]... arrays) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
for (byte[] array : arrays) {
outputStream.write(array);
}
} catch (IOException e) {
fail(e.getMessage());
}
return outputStream.toByteArray();
}
}