package org.limewire.bittorrent.bencoding; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import junit.framework.Test; import org.limewire.util.BEncoder; import org.limewire.util.BaseTestCase; import org.limewire.util.ReadBufferChannel; import org.limewire.util.StringUtils; @SuppressWarnings("unchecked") public class BEncodeTest extends BaseTestCase { public BEncodeTest(String name) { super(name); } public static Test suite() { return buildTestSuite(BEncodeTest.class); } private static class TestReadChannel implements ReadableByteChannel { public ByteBuffer src; public boolean closed; public void setString(String src) { setBytes(StringUtils.toAsciiBytes(src)); } public void setBytes(byte [] bytes) { this.src = ByteBuffer.wrap(bytes); closed = false; } public int read(ByteBuffer dst) throws IOException { if (!src.hasRemaining()) return closed ? -1 : 0; int position = src.position(); src.limit(Math.min(src.capacity(),src.position()+dst.remaining())); dst.put(src); src.limit(src.capacity()); return src.position() - position; } public void close() throws IOException { closed = true; } public boolean isOpen() { return !closed; } } static TestReadChannel chan = new TestReadChannel(); public void testTokenRecognition() throws Exception { // nothing chan.setString(""); Token t = Token.getNextToken(chan); assertNull(t); // long chan.setString("iSomething"); t = Token.getNextToken(chan); assertEquals(Token.LONG,t.getType()); assertEquals(1,chan.src.position()); // string chan.setString("1adf"); t = Token.getNextToken(chan); assertEquals(Token.STRING,t.getType()); assertEquals(1,chan.src.position()); // list chan.setString("ladf"); t = Token.getNextToken(chan); assertEquals(Token.LIST,t.getType()); assertEquals(1,chan.src.position()); // dictionary chan.setString("dadf"); t = Token.getNextToken(chan); assertEquals(Token.DICTIONARY,t.getType()); assertEquals(1,chan.src.position()); // boolean chan.setString("t"); t = Token.getNextToken(chan); assertEquals(Token.BOOLEAN, t.getType()); assertSame(BEBoolean.TRUE, t); assertEquals(Boolean.TRUE,t.getResult()); // rational chan.setString("r"); t = Token.getNextToken(chan); assertEquals(Token.RATIONAL, t.getType()); assertEquals(1,chan.src.position()); } /** * Tests some scenarios of creating a string. */ public void testString() throws Exception { // test a regular string chan.setString("4:asdf"); Token t = Token.getNextToken(chan); t.handleRead(); assertNotNull(t.getResult()); assertEquals(Token.STRING, t.getType()); assertEquals("asdf", StringUtils.getASCIIString((byte [])t.getResult())); // make sure the channel has been consumed completely assertFalse(chan.src.hasRemaining()); // repeat with some extra data to the channel chan.setString("4:asdfasdf"); t = Token.getNextToken(chan); t.handleRead(); assertNotNull(t.getResult()); assertEquals(Token.STRING, t.getType()); assertEquals("asdf", StringUtils.getASCIIString((byte [])t.getResult())); // there should be 4 bytes left in the channel assertEquals(4,chan.src.remaining()); // try sending the values byte by byte chan.setString("2"); t = Token.getNextToken(chan); t.handleRead(); assertNull(t.getResult()); chan.setString(":"); t.handleRead(); assertNull(t.getResult()); chan.setString("a"); t.handleRead(); assertNull(t.getResult()); chan.setString("s"); t.handleRead(); assertNotNull(t.getResult()); assertEquals("as",StringUtils.getASCIIString((byte[])t.getResult())); // try an empty read call chan.setString("2:a"); t = Token.getNextToken(chan); t.handleRead(); assertNull(t.getResult()); t.handleRead(); chan.setString("s"); t.handleRead(); assertNotNull(t.getResult()); assertEquals("as",StringUtils.getASCIIString((byte[])t.getResult())); // try a closed channel before end of string chan.setString("2:a"); chan.close(); t = Token.getNextToken(chan); try { t.handleRead(); fail("trying to read from closed channel should throw"); } catch (IOException expected){} // test an invalid string chan.setString("2;as"); t = Token.getNextToken(chan); assertEquals(Token.STRING,t.getType()); try { t.handleRead(); fail("invalid string should throw"); } catch (IOException expected){} assertEquals(2,chan.src.position()); // should stop reading after the invalid char. // test an empty string chan.setString("0:"); t = Token.getNextToken(chan); assertEquals(Token.STRING,t.getType()); t.handleRead(); assertEquals("",StringUtils.getASCIIString((byte[])t.getResult())); } /** * tests various scenarios of parsing a Long value. */ public void testLong() throws Exception { // test some empty calls chan.setString("i"); Token t = Token.getNextToken(chan); assertEquals(Token.LONG, t.getType()); t.handleRead(); assertNull(t.getResult()); t.handleRead(); assertNull(t.getResult()); // test adding the values digit by digit chan.setString("1"); t.handleRead(); assertNull(t.getResult()); chan.setString("2"); t.handleRead(); assertNull(t.getResult()); chan.setString("3"); t.handleRead(); assertNull(t.getResult()); chan.setString("e"); t.handleRead(); assertNotNull(t.getResult()); assertEquals(new Long(123),t.getResult()); // test having extra data in channel chan.setString("i123easdfadfs"); t = Token.getNextToken(chan); t.handleRead(); assertEquals(new Long(123), t.getResult()); assertEquals(5, chan.src.position()); assertTrue(chan.src.hasRemaining()); // 0 chan.setString("i0e"); t = Token.getNextToken(chan); t.handleRead(); assertEquals(new Long(0),t.getResult()); // negative values chan.setString("i-1e"); t = Token.getNextToken(chan); t.handleRead(); assertEquals(new Long(-1),t.getResult()); // leading 0s are invalid chan.setString("i001e"); t = Token.getNextToken(chan); try { t.handleRead(); fail(" leading 0s didn't throw"); } catch (IOException expected) {} assertNull(t.getResult()); assertEquals(3, chan.src.position()); // negative 0 is invalid chan.setString("i-01e"); t = Token.getNextToken(chan); try { t.handleRead(); fail(" negative 0 didn't throw"); } catch (IOException expected) {} assertNull(t.getResult()); assertEquals(3, chan.src.position()); // any non-numeric char is invalid chan.setString("i13i1e"); t = Token.getNextToken(chan); try { t.handleRead(); fail(" invalid chars didn't throw"); } catch (IOException expected) {} assertNull(t.getResult()); assertEquals(4, chan.src.position()); // too large or too small values chan.setString("i123487129587198257981374598173498751579813458345983297e"); t = Token.getNextToken(chan); try { t.handleRead(); fail(" too big didn't throw"); } catch (IOException expected) {} chan.setString("i-123487129587198257981374598173498751579813458345983297e"); t = Token.getNextToken(chan); try { t.handleRead(); fail(" too small didn't throw"); } catch (IOException expected) {} } /** * tests parsing of a rational value */ public void testRational() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); BEncoder b = BEncoder.getEncoder(baos); b.encodeRational(0.4); String encoded = StringUtils.getASCIIString(baos.toByteArray()); assertEquals("r4600877379321698714e",encoded); chan.setString(encoded); Double d = (Double)Token.parse(new ReadBufferChannel(baos.toByteArray())); assertEquals(0.4, d); } /** * test various scenarios of parsing a List */ public void testList() throws Exception { // few empty calls chan.setString("l"); Token t = Token.getNextToken(chan); assertEquals(Token.LIST, t.getType()); t.handleRead(); assertNull(t.getResult()); t.handleRead(); assertNull(t.getResult()); // create a list char at a time chan.setString("i5e"); t.handleRead(); assertNull(t.getResult()); chan.setString("e"); t.handleRead(); assertNotNull(t.getResult()); List l = (List)t.getResult(); assertEquals(1,l.size()); assertTrue(l.contains(new Long(5))); // an empty list chan.setString("le"); t = Token.getNextToken(chan); t.handleRead(); l = (List) t.getResult(); assertTrue(l.isEmpty()); // test that no extra data is read chan.setString("l2:asi44eei56e"); t = Token.getNextToken(chan); t.handleRead(); l = (List) t.getResult(); assertEquals(2, l.size()); assertTrue(l.contains(new StringByte("as"))); assertTrue(l.contains(new Long(44))); assertEquals(4, chan.src.remaining()); } /** * Tests various scenarios of parsing a dictionary */ public void testDictionary() throws Exception { // few empty calls chan.setString("d"); Token t = Token.getNextToken(chan); assertEquals(Token.DICTIONARY, t.getType()); t.handleRead(); assertNull(t.getResult()); t.handleRead(); assertNull(t.getResult()); // create a dictionary one char at a time chan.setString("2:as2:df"); t.handleRead(); assertNull(t.getResult()); chan.setString("e"); t.handleRead(); assertNotNull(t.getResult()); Map m = (Map)t.getResult(); assertEquals(1,m.size()); assertTrue(m.containsKey("as")); assertTrue(m.containsValue(new StringByte("df"))); // an empty dictionary chan.setString("de"); t = Token.getNextToken(chan); t.handleRead(); m = (Map)t.getResult(); assertTrue(m.isEmpty()); // no extra characters read chan.setString("d1:ai1eei45e"); t = Token.getNextToken(chan); t.handleRead(); m = (Map)t.getResult(); assertEquals(1,m.size()); assertTrue(chan.src.hasRemaining()); assertEquals(4, chan.src.remaining()); // invalid key chan.setString("di4ei5ee"); t = Token.getNextToken(chan); try { t.handleRead(); fail("non-string key didn't throw"); } catch (IOException expected){} // missing value chan.setString("d1:ae"); t = Token.getNextToken(chan); try { t.handleRead(); fail("missing value didn't throw"); } catch (IOException expected){} } /** * tests encoding and decoding a fancy nested collection structure */ public void testNested() throws Exception { // { key1 -> { key11 -> [badger, badger, 3, [mushroom, mushroom], {}], // key12 -> []} // key2 -> [[[[snake],snake]]] // key3 -> [true,[false,true,false]]} // key4 -> false List l = new ArrayList(); l.add("snake"); List l2 = new ArrayList(); l2.add(l); l2.add("snake"); l = new ArrayList(); l.add(l2); l2 = new ArrayList(); l2.add(l); List l3 = new ArrayList(); List l4 = new ArrayList(); l3.add(true); l4.add(false); l4.add(true); l4.add(false); l3.add(l4); Map m = new HashMap(); m.put("key2",l2); Map m2 = new HashMap(); m.put("key1",m2); m.put("key3",l3); m.put("key4", false); m2.put("key12",new ArrayList()); l = new ArrayList(); l.add("badger"); l.add("badger"); l.add(new Long(3)); l2 = new ArrayList(); l.add(l2); l.add(new HashMap()); l2.add("mushroom"); l2.add("mushroom"); m2.put("key11",l); // this finishes the fancy object ByteArrayOutputStream baos = new ByteArrayOutputStream(); BEncoder.getEncoder(baos, true, true, Token.ASCII).encodeDict(m); String s = new String(baos.toByteArray(),Token.ASCII); String expected = "d4:key1d5:key11l6:badger6:badgeri3el8:mushroom8:mushroomedee5:key12lee4:key2llll5:snakee5:snakeeee4:key3ltlftfee4:key4fe"; assertEquals(expected, s); chan.setString(s); Token t = Token.getNextToken(chan); t.handleRead(); Map outtest = (Map)t.getResult(); assertEquals(4, outtest.size()); Map inner = (Map)outtest.get("key1"); List empty = (List) inner.get("key12"); assertTrue(empty.isEmpty()); List badgers = (List) inner.get("key11"); assertEquals(5, badgers.size()); assertEquals(new StringByte("badger"),badgers.get(0)); assertEquals(new StringByte("badger"),badgers.get(1)); assertEquals(new Long(3),badgers.get(2)); List mushrooms = (List)badgers.get(3); assertEquals(2,mushrooms.size()); assertEquals(new StringByte("mushroom"),mushrooms.get(0)); assertEquals(new StringByte("mushroom"),mushrooms.get(1)); Map emptyMap = (Map)badgers.get(4); assertTrue(emptyMap.isEmpty()); List nestedList0 = (List)outtest.get("key2"); assertEquals(1,nestedList0.size()); List nestedList1 = (List)nestedList0.get(0); assertEquals(1,nestedList1.size()); List nestedList2 = (List)nestedList1.get(0); assertEquals(2,nestedList2.size()); assertTrue(nestedList2.contains(new StringByte("snake"))); List nestedList3 = (List) nestedList2.get(0); assertEquals(1,nestedList3.size()); assertTrue(nestedList3.contains(new StringByte("snake"))); List boolList = (List)outtest.get("key3"); assertEquals(2, boolList.size()); assertEquals(Boolean.TRUE,boolList.get(0)); List boolListInner = (List)boolList.get(1); assertEquals(3, boolListInner.size()); assertEquals(Boolean.FALSE,boolListInner.get(0)); assertEquals(Boolean.TRUE,boolListInner.get(1)); assertEquals(Boolean.FALSE,boolListInner.get(2)); assertEquals(Boolean.FALSE, outtest.get("key4")); } public void testUTF16() throws Exception { // a string with some utf16 characters String original = new String("A" + "\u00ea" + "\u00f1" + "\u00fc" +"\u1000"+ "C"); List<Object> l = new ArrayList<Object>(2); l.add(original); l.add(true); // first try to encode with ascii ByteArrayOutputStream baos = new ByteArrayOutputStream(); BEncoder.getEncoder(baos).encodeList(l); chan.setBytes(baos.toByteArray()); Token t = Token.getNextToken(chan); t.handleRead(); assertNotNull(t); assertEquals(Token.LIST, t.getType()); List parsed = (List)t.getResult(); byte [] parsedByte = (byte [])parsed.get(0); assertNotEquals(original, StringUtils.getASCIIString(parsedByte)); // the strings don't match assertNotEquals(original, new String(parsedByte,"UTF-8")); // not even if we know the encoding // then try to encode with UTF-16 baos = new ByteArrayOutputStream(); BEncoder.getEncoder(baos, false, false, "UTF-8").encodeList(l); chan.setBytes(baos.toByteArray()); t = Token.getNextToken(chan); t.handleRead(); assertNotNull(t); assertEquals(Token.LIST, t.getType()); parsed = (List)t.getResult(); parsedByte = (byte [])parsed.get(0); assertNotEquals(original, StringUtils.getASCIIString(parsedByte)); // the strings don't match with ascii assertEquals(original, new String(parsedByte,"UTF-8")); // but if we know the encoding they do } public void testEncodeIterables() throws Exception { Set<Long> s = new HashSet<Long>(); s.add(1L); s.add(2L); ByteArrayOutputStream baos = new ByteArrayOutputStream(); BEncoder.getEncoder(baos).encodeList(s); chan.setBytes(baos.toByteArray()); Token t = Token.getNextToken(chan); t.handleRead(); assertEquals(Token.LIST, t.getType()); List parsed = (List)t.getResult(); assertEquals(2, parsed.size()); System.out.println(parsed); assertTrue(s.containsAll(parsed)); } /** * Tests that dictionary keys can be encoded in UTF-8 and decoded properly. */ public void testEncodeDecodeDictonaryWithUTF8Keys() throws Exception { String key = String.valueOf('\u00e4'); Map<String, String> toEncode = Collections.singletonMap(key, key); ByteArrayOutputStream baos = new ByteArrayOutputStream(); BEncoder.getEncoder(baos, false, true, "UTF-8").encodeDict(toEncode); Map<String, Object> result = (Map<String, Object>) Token.parse(new ReadBufferChannel(baos.toByteArray()), "UTF-8"); String decodedKey = result.keySet().iterator().next(); assertEquals(key, decodedKey); byte[] value = (byte[]) result.values().iterator().next(); assertEquals(key, new String(value, "UTF-8")); } private static class StringByte { final String s; StringByte(String s) { this.s = s; } @Override public boolean equals(Object o) { if (!(o instanceof byte[])) return false; return s.equals(StringUtils.getASCIIString((byte[])o)); } } }