2010-03-03 Tatsuhiro Tsujikawa <t-tujikawa@users.sourceforge.net>

In Metalink4, if size element contains invalid size, discard whole
	document. Added strict hash value check for metalink3/4.
	* src/MetalinkParserController.cc
	* src/MetalinkParserStateMachine.cc
	* src/MetalinkParserStateMachine.h
	* src/MetalinkParserStateV3Impl.cc
	* src/MetalinkParserStateV4Impl.cc
	* src/util.cc
	* src/util.h
	* test/MetalinkParserControllerTest.cc
	* test/MetalinkProcessorTest.cc
	* test/metalink4.xml
pull/1/head
Tatsuhiro Tsujikawa 2010-03-02 15:14:39 +00:00
parent ba78b6f167
commit 3bb7855a56
11 changed files with 229 additions and 86 deletions

View File

@ -1,3 +1,18 @@
2010-03-03 Tatsuhiro Tsujikawa <t-tujikawa@users.sourceforge.net>
In Metalink4, if size element contains invalid size, discard whole
document. Added strict hash value check for metalink3/4.
* src/MetalinkParserController.cc
* src/MetalinkParserStateMachine.cc
* src/MetalinkParserStateMachine.h
* src/MetalinkParserStateV3Impl.cc
* src/MetalinkParserStateV4Impl.cc
* src/util.cc
* src/util.h
* test/MetalinkParserControllerTest.cc
* test/MetalinkProcessorTest.cc
* test/metalink4.xml
2010-03-02 Tatsuhiro Tsujikawa <t-tujikawa@users.sourceforge.net>
Added strict attribute validation for metalink4. When

View File

@ -53,6 +53,13 @@
namespace aria2 {
static bool isValidHash(const std::string& algo, const std::string& hash)
{
return util::isHexDigit(hash) &&
MessageDigestContext::supports(algo) &&
MessageDigestContext::digestLength(algo)*2 == hash.size();
}
MetalinkParserController::MetalinkParserController():
_metalinker(new Metalinker())
{}
@ -281,7 +288,11 @@ void MetalinkParserController::setHashOfChecksum(const std::string& md)
if(_tChecksum.isNull()) {
return;
}
_tChecksum->setMessageDigest(md);
if(isValidHash(_tChecksum->getAlgo(), md)) {
_tChecksum->setMessageDigest(md);
} else {
cancelChecksumTransaction();
}
#endif // ENABLE_MESSAGE_DIGEST
}
@ -352,7 +363,11 @@ void MetalinkParserController::addHashOfChunkChecksumV4(const std::string& md)
if(_tChunkChecksumV4.isNull()) {
return;
}
_tempChunkChecksumsV4.push_back(md);
if(isValidHash(_tChunkChecksumV4->getAlgo(), md)) {
_tempChunkChecksumsV4.push_back(md);
} else {
cancelChunkChecksumTransactionV4();
}
#endif // ENABLE_MESSAGE_DIGEST
}
@ -426,7 +441,11 @@ void MetalinkParserController::addHashOfChunkChecksum(size_t order, const std::s
if(_tChunkChecksum.isNull()) {
return;
}
_tempChunkChecksums.push_back(std::pair<size_t, std::string>(order, md));
if(isValidHash(_tChunkChecksum->getAlgo(), md)) {
_tempChunkChecksums.push_back(std::make_pair(order, md));
} else {
cancelChunkChecksumTransaction();
}
#endif // ENABLE_MESSAGE_DIGEST
}
@ -446,7 +465,11 @@ void MetalinkParserController::setMessageDigestOfChunkChecksum(const std::string
if(_tChunkChecksum.isNull()) {
return;
}
_tempHashPair.second = md;
if(isValidHash(_tChunkChecksum->getAlgo(), md)) {
_tempHashPair.second = md;
} else {
cancelChunkChecksumTransaction();
}
#endif // ENABLE_MESSAGE_DIGEST
}

View File

@ -290,6 +290,11 @@ void MetalinkParserStateMachine::commitEntryTransaction()
_ctrl->commitEntryTransaction();
}
void MetalinkParserStateMachine::cancelEntryTransaction()
{
_ctrl->cancelEntryTransaction();
}
void MetalinkParserStateMachine::newResourceTransaction()
{
_ctrl->newResourceTransaction();

View File

@ -166,6 +166,8 @@ public:
void commitEntryTransaction();
void cancelEntryTransaction();
void newResourceTransaction();
void setURLOfResource(const std::string& url);

View File

@ -183,7 +183,7 @@ void SizeMetalinkParserState::endElement
const std::string& characters)
{
try {
stm->setFileLengthOfEntry(util::parseLLInt(characters));
stm->setFileLengthOfEntry(util::parseULLInt(characters));
} catch(RecoverableException& e) {
// current metalink specification doesn't require size element.
}

View File

@ -281,9 +281,10 @@ void SizeMetalinkParserStateV4::endElement
const std::string& characters)
{
try {
stm->setFileLengthOfEntry(util::parseLLInt(characters));
stm->setFileLengthOfEntry(util::parseULLInt(characters));
} catch(RecoverableException& e) {
// current metalink specification doesn't require size element.
stm->cancelEntryTransaction();
stm->logError("Bad size");
}
}

View File

@ -216,6 +216,16 @@ bool isHexDigit(const char c)
return isDigit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f');
}
bool isHexDigit(const std::string& s)
{
for(std::string::const_iterator i = s.begin(), eoi = s.end(); i != eoi; ++i) {
if(!isHexDigit(*i)) {
return false;
}
}
return true;
}
bool inRFC3986ReservedChars(const char c)
{
static const char reserved[] = {

View File

@ -254,11 +254,15 @@ void sleep(long seconds);
void usleep(long microseconds);
bool isNumber(const std::string& what);
bool isHexDigit(const char c);
bool isHexDigit(const std::string& s);
bool isLowercase(const std::string& what);
bool isUppercase(const std::string& what);
/**
* Converts alphabets to unsigned int, assuming alphabets as a base 26
* integer and 'a' or 'A' is 0.

View File

@ -150,19 +150,27 @@ void MetalinkParserControllerTest::testChecksumTransaction()
ctrl.newEntryTransaction();
ctrl.newChecksumTransaction();
ctrl.setTypeOfChecksum("md5");
ctrl.setHashOfChecksum("hash");
ctrl.setHashOfChecksum("acbd18db4cc2f85cedef654fccc4a4d8");
ctrl.commitEntryTransaction();
{
SharedHandle<Metalinker> m = ctrl.getResult();
SharedHandle<Checksum> md = m->entries.front()->checksum;
CPPUNIT_ASSERT_EQUAL(std::string("md5"), md->getAlgo());
CPPUNIT_ASSERT_EQUAL(std::string("hash"), md->getMessageDigest());
CPPUNIT_ASSERT_EQUAL(std::string("acbd18db4cc2f85cedef654fccc4a4d8"),
md->getMessageDigest());
}
ctrl.newEntryTransaction();
ctrl.newChecksumTransaction();
ctrl.setTypeOfChecksum("md5");
ctrl.setHashOfChecksum("badhash");
ctrl.commitEntryTransaction();
CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->checksum.isNull());
ctrl.newEntryTransaction();
ctrl.newChecksumTransaction();
ctrl.cancelChecksumTransaction();
ctrl.commitEntryTransaction();
CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->checksum.isNull());
CPPUNIT_ASSERT(ctrl.getResult()->entries[2]->checksum.isNull());
}
void MetalinkParserControllerTest::testChunkChecksumTransaction()
@ -172,11 +180,11 @@ void MetalinkParserControllerTest::testChunkChecksumTransaction()
ctrl.newChunkChecksumTransaction();
ctrl.setTypeOfChunkChecksum("md5");
ctrl.setLengthOfChunkChecksum(256*1024);
ctrl.addHashOfChunkChecksum(4, "hash4");
ctrl.addHashOfChunkChecksum(1, "hash1");
ctrl.addHashOfChunkChecksum(3, "hash3");
ctrl.addHashOfChunkChecksum(2, "hash2");
ctrl.addHashOfChunkChecksum(5, "hash5");
ctrl.addHashOfChunkChecksum(4, "4cbd18db4cc2f85cedef654fccc4a4d8");
ctrl.addHashOfChunkChecksum(1, "1cbd18db4cc2f85cedef654fccc4a4d8");
ctrl.addHashOfChunkChecksum(3, "3cbd18db4cc2f85cedef654fccc4a4d8");
ctrl.addHashOfChunkChecksum(2, "2cbd18db4cc2f85cedef654fccc4a4d8");
ctrl.addHashOfChunkChecksum(5, "5cbd18db4cc2f85cedef654fccc4a4d8");
ctrl.commitEntryTransaction();
{
SharedHandle<Metalinker> m = ctrl.getResult();
@ -184,17 +192,30 @@ void MetalinkParserControllerTest::testChunkChecksumTransaction()
CPPUNIT_ASSERT_EQUAL(std::string("md5"), md->getAlgo());
CPPUNIT_ASSERT_EQUAL((size_t)256*1024, md->getChecksumLength());
CPPUNIT_ASSERT_EQUAL((size_t)5, md->countChecksum());
CPPUNIT_ASSERT_EQUAL(std::string("hash1"), md->getChecksums()[0]);
CPPUNIT_ASSERT_EQUAL(std::string("hash2"), md->getChecksums()[1]);
CPPUNIT_ASSERT_EQUAL(std::string("hash3"), md->getChecksums()[2]);
CPPUNIT_ASSERT_EQUAL(std::string("hash4"), md->getChecksums()[3]);
CPPUNIT_ASSERT_EQUAL(std::string("hash5"), md->getChecksums()[4]);
CPPUNIT_ASSERT_EQUAL(std::string("1cbd18db4cc2f85cedef654fccc4a4d8"),
md->getChecksums()[0]);
CPPUNIT_ASSERT_EQUAL(std::string("2cbd18db4cc2f85cedef654fccc4a4d8"),
md->getChecksums()[1]);
CPPUNIT_ASSERT_EQUAL(std::string("3cbd18db4cc2f85cedef654fccc4a4d8"),
md->getChecksums()[2]);
CPPUNIT_ASSERT_EQUAL(std::string("4cbd18db4cc2f85cedef654fccc4a4d8"),
md->getChecksums()[3]);
CPPUNIT_ASSERT_EQUAL(std::string("5cbd18db4cc2f85cedef654fccc4a4d8"),
md->getChecksums()[4]);
}
ctrl.newEntryTransaction();
ctrl.newChunkChecksumTransaction();
ctrl.setTypeOfChunkChecksum("md5");
ctrl.setLengthOfChunkChecksum(256*1024);
ctrl.addHashOfChunkChecksum(1, "badhash");
ctrl.commitEntryTransaction();
CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull());
ctrl.newEntryTransaction();
ctrl.newChunkChecksumTransaction();
ctrl.cancelChunkChecksumTransaction();
ctrl.commitEntryTransaction();
CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull());
CPPUNIT_ASSERT(ctrl.getResult()->entries[2]->chunkChecksum.isNull());
}
void MetalinkParserControllerTest::testChunkChecksumTransactionV4()
@ -202,27 +223,43 @@ void MetalinkParserControllerTest::testChunkChecksumTransactionV4()
MetalinkParserController ctrl;
ctrl.newEntryTransaction();
ctrl.newChunkChecksumTransactionV4();
ctrl.setTypeOfChunkChecksumV4("md5");
ctrl.setTypeOfChunkChecksumV4("sha-1");
ctrl.setLengthOfChunkChecksumV4(256*1024);
ctrl.addHashOfChunkChecksumV4("hash1");
ctrl.addHashOfChunkChecksumV4("hash2");
ctrl.addHashOfChunkChecksumV4("hash3");
ctrl.addHashOfChunkChecksumV4("5bd9f7248df0f3a6a86ab6c95f48787d546efa14");
ctrl.addHashOfChunkChecksumV4("9413ee70957a09d55704123687478e07f18c7b29");
ctrl.addHashOfChunkChecksumV4("44213f9f4d59b557314fadcd233232eebcac8012");
ctrl.commitEntryTransaction();
{
SharedHandle<Metalinker> m = ctrl.getResult();
SharedHandle<ChunkChecksum> md = m->entries.front()->chunkChecksum;
CPPUNIT_ASSERT_EQUAL(std::string("md5"), md->getAlgo());
CPPUNIT_ASSERT_EQUAL(std::string("sha-1"), md->getAlgo());
CPPUNIT_ASSERT_EQUAL((size_t)256*1024, md->getChecksumLength());
CPPUNIT_ASSERT_EQUAL((size_t)3, md->countChecksum());
CPPUNIT_ASSERT_EQUAL(std::string("hash1"), md->getChecksums()[0]);
CPPUNIT_ASSERT_EQUAL(std::string("hash2"), md->getChecksums()[1]);
CPPUNIT_ASSERT_EQUAL(std::string("hash3"), md->getChecksums()[2]);
CPPUNIT_ASSERT_EQUAL
(std::string("5bd9f7248df0f3a6a86ab6c95f48787d546efa14"),
md->getChecksums()[0]);
CPPUNIT_ASSERT_EQUAL
(std::string("9413ee70957a09d55704123687478e07f18c7b29"),
md->getChecksums()[1]);
CPPUNIT_ASSERT_EQUAL
(std::string("44213f9f4d59b557314fadcd233232eebcac8012"),
md->getChecksums()[2]);
}
ctrl.newEntryTransaction();
ctrl.newChunkChecksumTransactionV4();
ctrl.setTypeOfChunkChecksumV4("sha-1");
ctrl.setLengthOfChunkChecksumV4(256*1024);
ctrl.addHashOfChunkChecksumV4("5bd9f7248df0f3a6a86ab6c95f48787d546efa14");
ctrl.addHashOfChunkChecksumV4("badhash");
ctrl.commitEntryTransaction();
CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull());
ctrl.newEntryTransaction();
ctrl.newChunkChecksumTransactionV4();
ctrl.cancelChunkChecksumTransactionV4();
ctrl.commitEntryTransaction();
CPPUNIT_ASSERT(ctrl.getResult()->entries[1]->chunkChecksum.isNull());
CPPUNIT_ASSERT(ctrl.getResult()->entries[2]->chunkChecksum.isNull());
}
#endif // ENABLE_MESSAGE_DIGEST

View File

@ -33,6 +33,7 @@ class MetalinkProcessorTest:public CppUnit::TestFixture {
CPPUNIT_TEST(testMalformedXML);
CPPUNIT_TEST(testMalformedXML2);
CPPUNIT_TEST(testBadSize);
CPPUNIT_TEST(testBadSizeV4);
CPPUNIT_TEST(testBadMaxConn);
CPPUNIT_TEST(testNoName);
CPPUNIT_TEST(testBadURLPrefs);
@ -58,6 +59,7 @@ public:
void testMalformedXML();
void testMalformedXML2();
void testBadSize();
void testBadSizeV4();
void testBadMaxConn();
void testNoName();
void testBadURLPrefs();
@ -90,7 +92,7 @@ void MetalinkProcessorTest::testParseFileV4()
CPPUNIT_ASSERT_EQUAL((uint64_t)786430LL, e->getLength());
CPPUNIT_ASSERT_EQUAL(-1, e->maxConnections);
#ifdef ENABLE_MESSAGE_DIGEST
CPPUNIT_ASSERT_EQUAL(std::string("80bc95fd391772fa61c91ed68567f0980bb45fd9"),
CPPUNIT_ASSERT_EQUAL(std::string("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"),
e->checksum->getMessageDigest());
CPPUNIT_ASSERT(!e->checksum.isNull());
CPPUNIT_ASSERT_EQUAL(std::string("sha-1"), e->checksum->getAlgo());
@ -98,11 +100,11 @@ void MetalinkProcessorTest::testParseFileV4()
CPPUNIT_ASSERT_EQUAL(std::string("sha-256"), e->chunkChecksum->getAlgo());
CPPUNIT_ASSERT_EQUAL((size_t)262144, e->chunkChecksum->getChecksumLength());
CPPUNIT_ASSERT_EQUAL((size_t)3, e->chunkChecksum->countChecksum());
CPPUNIT_ASSERT_EQUAL(std::string("metalinkhash1"),
CPPUNIT_ASSERT_EQUAL(std::string("0245178074fd042e19b7c3885b360fc21064b30e73f5626c7e3b005d048069c5"),
e->chunkChecksum->getChecksum(0));
CPPUNIT_ASSERT_EQUAL(std::string("metalinkhash2"),
CPPUNIT_ASSERT_EQUAL(std::string("487ba2299be7f759d7c7bf6a4ac3a32cee81f1bb9332fc485947e32918864fb2"),
e->chunkChecksum->getChecksum(1));
CPPUNIT_ASSERT_EQUAL(std::string("metalinkhash3"),
CPPUNIT_ASSERT_EQUAL(std::string("37290d74ac4d186e3a8e5785d259d2ec04fac91ae28092e7620ec8bc99e830aa"),
e->chunkChecksum->getChecksum(2));
#endif // ENABLE_MESSAGE_DIGEST
CPPUNIT_ASSERT(!e->getSignature().isNull());
@ -202,6 +204,11 @@ void MetalinkProcessorTest::testParseFileV4_attrs()
} catch(RecoverableException& e) {
// success
}
dw->setString(StringFormat(tmpl, "A").str());
try {
m = proc.parseFromBinaryStream(dw);
CPPUNIT_FAIL("exception must be thrown.");
} catch(RecoverableException& e) {}
}
{
// Testing metaurl@priority
@ -239,6 +246,11 @@ void MetalinkProcessorTest::testParseFileV4_attrs()
} catch(RecoverableException& e) {
// success
}
dw->setString(StringFormat(tmpl, "A").str());
try {
m = proc.parseFromBinaryStream(dw);
CPPUNIT_FAIL("exception must be thrown.");
} catch(RecoverableException& e) {}
}
{
// Testing metaurl@mediatype
@ -346,6 +358,12 @@ void MetalinkProcessorTest::testParseFileV4_attrs()
m = proc.parseFromBinaryStream(dw);
CPPUNIT_FAIL("exception must be thrown.");
} catch(RecoverableException& e) {}
// not a number
try {
dw->setString(StringFormat(tmpl, "A").str());
m = proc.parseFromBinaryStream(dw);
CPPUNIT_FAIL("exception must be thrown.");
} catch(RecoverableException& e) {}
}
{
// Testing pieces@type
@ -622,6 +640,31 @@ void MetalinkProcessorTest::testMalformedXML2()
}
}
void MetalinkProcessorTest::testBadSizeV4()
{
MetalinkProcessor proc;
SharedHandle<Metalinker> m;
SharedHandle<ByteArrayDiskWriter> dw(new ByteArrayDiskWriter());
const char* tmpl =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<metalink xmlns=\"urn:ietf:params:xml:ns:metalink\">"
"<file name=\"foo\">"
"<size>%s</size>"
"<url>http://example.org</url>"
"</file>"
"</metalink>";
dw->setString(StringFormat(tmpl, "9223372036854775807").str());
m = proc.parseFromBinaryStream(dw);
dw->setString(StringFormat(tmpl, "-1").str());
try {
m = proc.parseFromBinaryStream(dw);
CPPUNIT_FAIL("exception must be thrown.");
} catch(RecoverableException& e) {}
}
void MetalinkProcessorTest::testBadSize()
{
MetalinkProcessor proc;
@ -850,21 +893,22 @@ void MetalinkProcessorTest::testBadPieceNo()
{
MetalinkProcessor proc;
SharedHandle<ByteArrayDiskWriter> dw(new ByteArrayDiskWriter());
dw->setString("<metalink version=\"3.0\" xmlns=\"http://www.metalinker.org/\">"
"<files>"
"<file name=\"aria2.tar.bz2\">"
" <verification>"
" <pieces length=\"512\" type=\"sha1\">"
" <hash piece=\"0\">abc</hash>"
" <hash piece=\"xyz\">xyz</hash>"
" </pieces>"
" <pieces length=\"1024\" type=\"sha1\">"
" <hash piece=\"0\">abc</hash>"
" </pieces>"
" </verification>"
"</file>"
"</files>"
"</metalink>");
dw->setString
("<metalink version=\"3.0\" xmlns=\"http://www.metalinker.org/\">"
"<files>"
"<file name=\"aria2.tar.bz2\">"
" <verification>"
" <pieces length=\"512\" type=\"sha1\">"
" <hash piece=\"0\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" <hash piece=\"xyz\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" </pieces>"
" <pieces length=\"1024\" type=\"sha1\">"
" <hash piece=\"0\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" </pieces>"
" </verification>"
"</file>"
"</files>"
"</metalink>");
try {
SharedHandle<Metalinker> m = proc.parseFromBinaryStream(dw);
@ -883,20 +927,21 @@ void MetalinkProcessorTest::testBadPieceLength()
{
MetalinkProcessor proc;
SharedHandle<ByteArrayDiskWriter> dw(new ByteArrayDiskWriter());
dw->setString("<metalink version=\"3.0\" xmlns=\"http://www.metalinker.org/\">"
"<files>"
"<file name=\"aria2.tar.bz2\">"
" <verification>"
" <pieces length=\"xyz\" type=\"sha1\">"
" <hash piece=\"0\">abc</hash>"
" </pieces>"
" <pieces length=\"1024\" type=\"sha1\">"
" <hash piece=\"0\">abc</hash>"
" </pieces>"
" </verification>"
"</file>"
"</files>"
"</metalink>");
dw->setString
("<metalink version=\"3.0\" xmlns=\"http://www.metalinker.org/\">"
"<files>"
"<file name=\"aria2.tar.bz2\">"
" <verification>"
" <pieces length=\"xyz\" type=\"sha1\">"
" <hash piece=\"0\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" </pieces>"
" <pieces length=\"1024\" type=\"sha1\">"
" <hash piece=\"0\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" </pieces>"
" </verification>"
"</file>"
"</files>"
"</metalink>");
try {
SharedHandle<Metalinker> m = proc.parseFromBinaryStream(dw);
@ -915,20 +960,21 @@ void MetalinkProcessorTest::testUnsupportedType_piece()
{
MetalinkProcessor proc;
SharedHandle<ByteArrayDiskWriter> dw(new ByteArrayDiskWriter());
dw->setString("<metalink version=\"3.0\" xmlns=\"http://www.metalinker.org/\">"
"<files>"
"<file name=\"aria2.tar.bz2\">"
" <verification>"
" <pieces length=\"512\" type=\"ARIA2\">"
" <hash piece=\"0\">abc</hash>"
" </pieces>"
" <pieces length=\"1024\" type=\"sha1\">"
" <hash piece=\"0\">abc</hash>"
" </pieces>"
" </verification>"
"</file>"
"</files>"
"</metalink>");
dw->setString
("<metalink version=\"3.0\" xmlns=\"http://www.metalinker.org/\">"
"<files>"
"<file name=\"aria2.tar.bz2\">"
" <verification>"
" <pieces length=\"512\" type=\"ARIA2\">"
" <hash piece=\"0\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" </pieces>"
" <pieces length=\"1024\" type=\"sha1\">"
" <hash piece=\"0\">44213f9f4d59b557314fadcd233232eebcac8012</hash>"
" </pieces>"
" </verification>"
"</file>"
"</files>"
"</metalink>");
try {
SharedHandle<Metalinker> m = proc.parseFromBinaryStream(dw);

View File

@ -7,17 +7,17 @@
<version>1.0</version>
<language>en</language>
<description>A description of the example file for download.</description>
<hash type="md5">80bc95fd391772fa61c91ed68567f0980bb45fd9</hash>
<hash type="sha-1">80bc95fd391772fa61c91ed68567f0980bb45fd9</hash>
<hash type="md5">cbd18db4cc2f85cedef654fccc4a4d8</hash>
<hash type="sha-1">0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33</hash>
<pieces length="262144" type="sha-1">
<hash>metalinkhash1</hash>
<hash>metalinkhash2</hash>
<hash>metalinkhash3</hash>
<hash>5bd9f7248df0f3a6a86ab6c95f48787d546efa14</hash>
<hash>9413ee70957a09d55704123687478e07f18c7b29</hash>
<hash>44213f9f4d59b557314fadcd233232eebcac8012</hash>
</pieces>
<pieces length="262144" type="sha-256">
<hash>metalinkhash1</hash>
<hash>metalinkhash2</hash>
<hash>metalinkhash3</hash>
<hash>0245178074fd042e19b7c3885b360fc21064b30e73f5626c7e3b005d048069c5</hash>
<hash>487ba2299be7f759d7c7bf6a4ac3a32cee81f1bb9332fc485947e32918864fb2</hash>
<hash>37290d74ac4d186e3a8e5785d259d2ec04fac91ae28092e7620ec8bc99e830aa</hash>
</pieces>
<url location="de" priority="1">ftp://ftp.example.com/example.ext</url>
<url location="fr" priority="1">http://example.com/example.ext</url>