diff --git a/test/Metadata.cpp b/test/Metadata.cpp index 41e98b486..b0f3c5994 100644 --- a/test/Metadata.cpp +++ b/test/Metadata.cpp @@ -21,6 +21,8 @@ #include #include +#include +#include #include #include @@ -31,7 +33,7 @@ namespace dev namespace test { -bytes bytecodeSansMetadata(bytes const& _bytecode) +bytes onlyMetadata(bytes const& _bytecode) { unsigned size = _bytecode.size(); if (size < 5) @@ -43,6 +45,14 @@ bytes bytecodeSansMetadata(bytes const& _bytecode) unsigned char firstByte = _bytecode[size - metadataSize - 2]; if (firstByte != 0xa1 && firstByte != 0xa2) return bytes{}; + return bytes(_bytecode.end() - metadataSize - 2, _bytecode.end() - 2); +} + +bytes bytecodeSansMetadata(bytes const& _bytecode) +{ + unsigned metadataSize = onlyMetadata(_bytecode).size(); + if (metadataSize == 0) + return bytes{}; return bytes(_bytecode.begin(), _bytecode.end() - metadataSize - 2); } @@ -51,6 +61,116 @@ string bytecodeSansMetadata(string const& _bytecode) return toHex(bytecodeSansMetadata(fromHex(_bytecode, WhenError::Throw))); } +DEV_SIMPLE_EXCEPTION(CBORException); + +class TinyCBORParser +{ +public: + explicit TinyCBORParser(bytes const& _metadata): m_pos(0), m_metadata(_metadata) + { + assertThrow((m_pos + 1) < _metadata.size(), CBORException, "Input too short."); + } + unsigned mapItemCount() + { + assertThrow(nextType() == MajorType::Map, CBORException, "Fixed-length map expected."); + return readLength(); + } + string readKey() + { + return readString(); + } + string readValue() + { + switch(nextType()) + { + case MajorType::ByteString: + return toHex(readBytes(readLength())); + case MajorType::TextString: + return readString(); + case MajorType::SimpleData: + { + unsigned value = nextImmediate(); + m_pos++; + if (value == 20) + return "false"; + else if (value == 21) + return "true"; + else + assertThrow(false, CBORException, "Unsupported simple value (not a boolean)."); + } + default: + assertThrow(false, CBORException, "Unsupported value type."); + } + } +private: + enum class MajorType + { + ByteString, + TextString, + Map, + SimpleData + }; + MajorType nextType() const + { + unsigned value = (m_metadata.at(m_pos) >> 5) & 0x7; + switch (value) + { + case 2: return MajorType::ByteString; + case 3: return MajorType::TextString; + case 5: return MajorType::Map; + case 7: return MajorType::SimpleData; + default: assertThrow(false, CBORException, "Unsupported major type."); + } + } + unsigned nextImmediate() const { return m_metadata.at(m_pos) & 0x1f; } + unsigned readLength() + { + unsigned length = m_metadata.at(m_pos++) & 0x1f; + if (length < 24) + return length; + if (length == 24) + return m_metadata.at(m_pos++); + // Unsupported length kind. (Only by this parser.) + assertThrow(false, CBORException, string("Unsupported length ") + to_string(length)); + } + bytes readBytes(unsigned length) + { + bytes ret{m_metadata.begin() + m_pos, m_metadata.begin() + m_pos + length}; + m_pos += length; + return ret; + } + string readString() + { + // Expect a text string. + assertThrow(nextType() == MajorType::TextString, CBORException, "String expected."); + bytes tmp{readBytes(readLength())}; + return string{tmp.begin(), tmp.end()}; + } + unsigned m_pos; + bytes const& m_metadata; +}; + +boost::optional> parseCBORMetadata(bytes const& _metadata) +{ + try + { + TinyCBORParser parser(_metadata); + map ret; + unsigned count = parser.mapItemCount(); + for (unsigned i = 0; i < count; i++) + { + string key = parser.readKey(); + string value = parser.readValue(); + ret[move(key)] = move(value); + } + return ret; + } + catch (CBORException const&) + { + return {}; + } +} + bool isValidMetadata(string const& _metadata) { Json::Value metadata; diff --git a/test/Metadata.h b/test/Metadata.h index 113a54bee..4c7a10b31 100644 --- a/test/Metadata.h +++ b/test/Metadata.h @@ -20,6 +20,10 @@ */ #include + +#include + +#include #include namespace dev @@ -27,6 +31,9 @@ namespace dev namespace test { +/// Returns only the CBOR metadata. +bytes onlyMetadata(bytes const& _bytecode); + /// Returns the bytecode with the metadata hash stripped out. bytes bytecodeSansMetadata(bytes const& _bytecode); @@ -34,6 +41,15 @@ bytes bytecodeSansMetadata(bytes const& _bytecode); /// Throws exception on invalid hex string. std::string bytecodeSansMetadata(std::string const& _bytecode); +/// Parse CBOR metadata into a map. Expects the input CBOR to be a +/// fixed length map, with each key being a string. The values +/// are parsed as follows: +/// - strings into strings +/// - bytes into hex strings +/// - booleans into "true"/"false" strings +/// - everything else is invalid +boost::optional> parseCBORMetadata(bytes const& _metadata); + /// Expects a serialised metadata JSON and returns true if the /// content is valid metadata. bool isValidMetadata(std::string const& _metadata); diff --git a/test/libsolidity/Metadata.cpp b/test/libsolidity/Metadata.cpp index 007ee2b6b..d1e0cd678 100644 --- a/test/libsolidity/Metadata.cpp +++ b/test/libsolidity/Metadata.cpp @@ -25,6 +25,8 @@ #include #include +using namespace std; + namespace dev { namespace solidity @@ -32,6 +34,18 @@ namespace solidity namespace test { +namespace +{ +map requireParsedCBORMetadata(bytes const& _bytecode) +{ + bytes cborMetadata = dev::test::onlyMetadata(_bytecode); + BOOST_REQUIRE(!cborMetadata.empty()); + boost::optional> tmp = dev::test::parseCBORMetadata(cborMetadata); + BOOST_REQUIRE(tmp); + return *tmp; +} +} + BOOST_AUTO_TEST_SUITE(Metadata) BOOST_AUTO_TEST_CASE(metadata_stamp) @@ -54,11 +68,10 @@ BOOST_AUTO_TEST_CASE(metadata_stamp) BOOST_CHECK(dev::test::isValidMetadata(metadata)); bytes hash = dev::swarmHash(metadata).asBytes(); BOOST_REQUIRE(hash.size() == 32); - BOOST_REQUIRE(bytecode.size() >= 2); - size_t metadataCBORSize = (size_t(bytecode.end()[-2]) << 8) + size_t(bytecode.end()[-1]); - BOOST_REQUIRE(metadataCBORSize < bytecode.size() - 2); - bytes expectation = bytes{0xa1, 0x65, 'b', 'z', 'z', 'r', '0', 0x58, 0x20} + hash; - BOOST_CHECK(std::equal(expectation.begin(), expectation.end(), bytecode.end() - metadataCBORSize - 2)); + auto const cborMetadata = requireParsedCBORMetadata(bytecode); + BOOST_CHECK(cborMetadata.size() == 1); + BOOST_CHECK(cborMetadata.count("bzzr0") == 1); + BOOST_CHECK(cborMetadata.at("bzzr0") == toHex(hash)); } BOOST_AUTO_TEST_CASE(metadata_stamp_experimental) @@ -81,14 +94,12 @@ BOOST_AUTO_TEST_CASE(metadata_stamp_experimental) BOOST_CHECK(dev::test::isValidMetadata(metadata)); bytes hash = dev::swarmHash(metadata).asBytes(); BOOST_REQUIRE(hash.size() == 32); - BOOST_REQUIRE(bytecode.size() >= 2); - size_t metadataCBORSize = (size_t(bytecode.end()[-2]) << 8) + size_t(bytecode.end()[-1]); - BOOST_REQUIRE(metadataCBORSize < bytecode.size() - 2); - bytes expectation = - bytes{0xa2, 0x65, 'b', 'z', 'z', 'r', '0', 0x58, 0x20} + - hash + - bytes{0x6c, 'e', 'x', 'p', 'e', 'r', 'i', 'm', 'e', 'n', 't', 'a', 'l', 0xf5}; - BOOST_CHECK(std::equal(expectation.begin(), expectation.end(), bytecode.end() - metadataCBORSize - 2)); + auto const cborMetadata = requireParsedCBORMetadata(bytecode); + BOOST_CHECK(cborMetadata.size() == 2); + BOOST_CHECK(cborMetadata.count("bzzr0") == 1); + BOOST_CHECK(cborMetadata.at("bzzr0") == toHex(hash)); + BOOST_CHECK(cborMetadata.count("experimental") == 1); + BOOST_CHECK(cborMetadata.at("experimental") == "true"); } BOOST_AUTO_TEST_CASE(metadata_relevant_sources)