Merge pull request #4942 from ByteHamster/rewrite-chapter

Rewrite chapter parser for testability
This commit is contained in:
ByteHamster 2021-02-16 22:14:06 +01:00 committed by GitHub
commit cf2d1006df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 428 additions and 332 deletions

View File

@ -95,14 +95,9 @@ public class ChapterUtils {
@NonNull
private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
ChapterReader reader = new ChapterReader();
reader.readInputStream(in);
ChapterReader reader = new ChapterReader(in);
reader.readInputStream();
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());
enumerateEmptyChapterTitles(chapters);
if (!chaptersValid(chapters)) {
@ -118,7 +113,6 @@ public class ChapterUtils {
reader.readInputStream(input);
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());

View File

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

View File

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

View File

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

View File

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