OSDN Git Service

AAPT2: Verify min/max attr fields
authorAdam Lesinski <adamlesinski@google.com>
Fri, 20 Nov 2015 23:32:30 +0000 (15:32 -0800)
committerAdam Lesinski <adamlesinski@google.com>
Mon, 23 Nov 2015 20:01:15 +0000 (12:01 -0800)
Integers are now checked to see if they fall in the range
of min/max for the attribute they are assigned.

Change-Id: I42c435b15fd3f0bd23691c83efccce4ad5973276

14 files changed:
tools/aapt2/Diagnostics.h
tools/aapt2/ResourceParser.cpp
tools/aapt2/ResourceParser_test.cpp
tools/aapt2/ResourceValues.cpp
tools/aapt2/ResourceValues.h
tools/aapt2/Source.h
tools/aapt2/flatten/TableFlattener.cpp
tools/aapt2/flatten/TableFlattener_test.cpp
tools/aapt2/link/ReferenceLinker.cpp
tools/aapt2/process/SymbolTable.cpp
tools/aapt2/test/Builders.h
tools/aapt2/unflatten/BinaryResourceParser.cpp
tools/aapt2/util/Maybe.h
tools/aapt2/util/Maybe_test.cpp

index 7ea26b3..ab4d284 100644 (file)
@@ -18,7 +18,6 @@
 #define AAPT_DIAGNOSTICS_H
 
 #include "Source.h"
-
 #include "util/StringPiece.h"
 #include "util/Util.h"
 
index 02fe59c..f6a720a 100644 (file)
@@ -711,6 +711,46 @@ bool ResourceParser::parseAttrImpl(xml::XmlPullParser* parser, ParsedResource* o
         }
     }
 
+    Maybe<int32_t> maybeMin, maybeMax;
+
+    if (Maybe<StringPiece16> maybeMinStr = xml::findAttribute(parser, u"min")) {
+        StringPiece16 minStr = util::trimWhitespace(maybeMinStr.value());
+        if (!minStr.empty()) {
+            android::Res_value value;
+            if (android::ResTable::stringToInt(minStr.data(), minStr.size(), &value)) {
+                maybeMin = static_cast<int32_t>(value.data);
+            }
+        }
+
+        if (!maybeMin) {
+            mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
+                         << "invalid 'min' value '" << minStr << "'");
+            return false;
+        }
+    }
+
+    if (Maybe<StringPiece16> maybeMaxStr = xml::findAttribute(parser, u"max")) {
+        StringPiece16 maxStr = util::trimWhitespace(maybeMaxStr.value());
+        if (!maxStr.empty()) {
+            android::Res_value value;
+            if (android::ResTable::stringToInt(maxStr.data(), maxStr.size(), &value)) {
+                maybeMax = static_cast<int32_t>(value.data);
+            }
+        }
+
+        if (!maybeMax) {
+            mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
+                         << "invalid 'max' value '" << maxStr << "'");
+            return false;
+        }
+    }
+
+    if ((maybeMin || maybeMax) && (typeMask & android::ResTable_map::TYPE_INTEGER) == 0) {
+        mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
+                     << "'min' and 'max' can only be used when format='integer'");
+        return false;
+    }
+
     struct SymbolComparator {
         bool operator()(const Attribute::Symbol& a, const Attribute::Symbol& b) {
             return a.symbol.name.value() < b.symbol.name.value();
@@ -794,6 +834,13 @@ bool ResourceParser::parseAttrImpl(xml::XmlPullParser* parser, ParsedResource* o
     std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(weak);
     attr->symbols = std::vector<Attribute::Symbol>(items.begin(), items.end());
     attr->typeMask = typeMask ? typeMask : uint32_t(android::ResTable_map::TYPE_ANY);
+    if (maybeMin) {
+        attr->minInt = maybeMin.value();
+    }
+
+    if (maybeMax) {
+        attr->maxInt = maybeMax.value();
+    }
     outResource->value = std::move(attr);
     return true;
 }
index ab16424..6e0812b 100644 (file)
@@ -138,6 +138,22 @@ TEST_F(ResourceParserTest, ParseAttr) {
     EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_ANY), attr->typeMask);
 }
 
+TEST_F(ResourceParserTest, ParseAttrWithMinMax) {
+    std::string input = "<attr name=\"foo\" min=\"10\" max=\"23\" format=\"integer\"/>";
+    ASSERT_TRUE(testParse(input));
+
+    Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/foo");
+    ASSERT_NE(nullptr, attr);
+    EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_INTEGER), attr->typeMask);
+    EXPECT_EQ(10, attr->minInt);
+    EXPECT_EQ(23, attr->maxInt);
+}
+
+TEST_F(ResourceParserTest, FailParseAttrWithMinMaxButNotInteger) {
+    std::string input = "<attr name=\"foo\" min=\"10\" max=\"23\" format=\"string\"/>";
+    ASSERT_FALSE(testParse(input));
+}
+
 TEST_F(ResourceParserTest, ParseUseAndDeclOfAttr) {
     std::string input = "<declare-styleable name=\"Styleable\">\n"
                         "  <attr name=\"foo\" />\n"
index 5550f19..04c375f 100644 (file)
@@ -15,9 +15,9 @@
  */
 
 #include "Resource.h"
+#include "ResourceUtils.h"
 #include "ResourceValues.h"
 #include "ValueVisitor.h"
-
 #include "util/Util.h"
 #include "flatten/ResourceTypeExtensions.h"
 
@@ -216,7 +216,7 @@ void BinaryPrimitive::print(std::ostream* out) const {
             *out << "(null)";
             break;
         case android::Res_value::TYPE_INT_DEC:
-            *out << "(integer) " << value.data;
+            *out << "(integer) " << static_cast<int32_t>(value.data);
             break;
         case android::Res_value::TYPE_INT_HEX:
             *out << "(integer) " << std::hex << value.data << std::dec;
@@ -237,7 +237,10 @@ void BinaryPrimitive::print(std::ostream* out) const {
     }
 }
 
-Attribute::Attribute(bool w, uint32_t t) : weak(w), typeMask(t) {
+Attribute::Attribute(bool w, uint32_t t) :
+        weak(w), typeMask(t),
+        minInt(std::numeric_limits<int32_t>::min()),
+        maxInt(std::numeric_limits<int32_t>::max()) {
 }
 
 bool Attribute::isWeak() const {
@@ -361,6 +364,81 @@ void Attribute::print(std::ostream* out) const {
     }
 }
 
+static void buildAttributeMismatchMessage(DiagMessage* msg, const Attribute* attr,
+                                          const Item* value) {
+    *msg << "expected";
+    if (attr->typeMask & android::ResTable_map::TYPE_BOOLEAN) {
+        *msg << " boolean";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_COLOR) {
+        *msg << " color";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_DIMENSION) {
+        *msg << " dimension";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_ENUM) {
+        *msg << " enum";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_FLAGS) {
+        *msg << " flags";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_FLOAT) {
+        *msg << " float";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_FRACTION) {
+        *msg << " fraction";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_INTEGER) {
+        *msg << " integer";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_REFERENCE) {
+        *msg << " reference";
+    }
+
+    if (attr->typeMask & android::ResTable_map::TYPE_STRING) {
+        *msg << " string";
+    }
+
+    *msg << " but got " << *value;
+}
+
+bool Attribute::matches(const Item* item, DiagMessage* outMsg) const {
+    android::Res_value val = {};
+    item->flatten(&val);
+
+    // Always allow references.
+    const uint32_t mask = typeMask | android::ResTable_map::TYPE_REFERENCE;
+    if (!(mask & ResourceUtils::androidTypeToAttributeTypeMask(val.dataType))) {
+        if (outMsg) {
+            buildAttributeMismatchMessage(outMsg, this, item);
+        }
+        return false;
+
+    } else if (ResourceUtils::androidTypeToAttributeTypeMask(val.dataType) &
+            android::ResTable_map::TYPE_INTEGER) {
+        if (static_cast<int32_t>(util::deviceToHost32(val.data)) < minInt) {
+            if (outMsg) {
+                *outMsg << *item << " is less than minimum integer " << minInt;
+            }
+            return false;
+        } else if (static_cast<int32_t>(util::deviceToHost32(val.data)) > maxInt) {
+            if (outMsg) {
+                *outMsg << *item << " is greater than maximum integer " << maxInt;
+            }
+            return false;
+        }
+    }
+    return true;
+}
+
 Style* Style::clone(StringPool* newPool) const {
     Style* style = new Style();
     style->parent = parent;
index 7ae346a..a038282 100644 (file)
 #ifndef AAPT_RESOURCE_VALUES_H
 #define AAPT_RESOURCE_VALUES_H
 
-#include "util/Maybe.h"
+#include "Diagnostics.h"
 #include "Resource.h"
 #include "StringPool.h"
+#include "util/Maybe.h"
 
 #include <array>
 #include <androidfw/ResourceTypes.h>
@@ -233,8 +234,8 @@ struct Attribute : public BaseValue<Attribute> {
 
        bool weak;
     uint32_t typeMask;
-    uint32_t minInt;
-    uint32_t maxInt;
+    int32_t minInt;
+    int32_t maxInt;
     std::vector<Symbol> symbols;
 
     Attribute(bool w, uint32_t t = 0u);
@@ -243,6 +244,7 @@ struct Attribute : public BaseValue<Attribute> {
     Attribute* clone(StringPool* newPool) const override;
     void printMask(std::ostream* out) const;
     void print(std::ostream* out) const override;
+    bool matches(const Item* item, DiagMessage* outMsg) const;
 };
 
 struct Style : public BaseValue<Style> {
index 8af203c..319528e 100644 (file)
@@ -58,6 +58,10 @@ inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) {
     return out;
 }
 
+inline bool operator==(const Source& lhs, const Source& rhs) {
+    return lhs.path == rhs.path && lhs.line == rhs.line;
+}
+
 inline bool operator<(const Source& lhs, const Source& rhs) {
     int cmp = lhs.path.compare(rhs.path);
     if (cmp < 0) return true;
index 636e977..0590610 100644 (file)
@@ -165,6 +165,18 @@ struct MapFlattenVisitor : public RawValueVisitor {
             flattenEntry(&key, &val);
         }
 
+        if (attr->minInt != std::numeric_limits<int32_t>::min()) {
+            Reference key(ResourceId{ ResTable_map::ATTR_MIN });
+            BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->minInt));
+            flattenEntry(&key, &val);
+        }
+
+        if (attr->maxInt != std::numeric_limits<int32_t>::max()) {
+            Reference key(ResourceId{ ResTable_map::ATTR_MAX });
+            BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->maxInt));
+            flattenEntry(&key, &val);
+        }
+
         for (Attribute::Symbol& s : attr->symbols) {
             BinaryPrimitive val(Res_value::TYPE_INT_DEC, s.value);
             flattenEntry(&s.symbol, &val);
index 4ffb980..7030603 100644 (file)
@@ -262,4 +262,55 @@ TEST_F(TableFlattenerTest, FlattenUnlinkedTable) {
     EXPECT_EQ(ref->name.value(), test::parseNameOrDie(u"@com.app.test:color/green"));
 }
 
+TEST_F(TableFlattenerTest, FlattenMinMaxAttributes) {
+    Attribute attr(false);
+    attr.typeMask = android::ResTable_map::TYPE_INTEGER;
+    attr.minInt = 10;
+    attr.maxInt = 23;
+    std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+            .setPackageId(u"android", 0x01)
+            .addValue(u"@android:attr/foo", ResourceId(0x01010000),
+                      util::make_unique<Attribute>(attr))
+            .build();
+
+    ResourceTable result;
+    ASSERT_TRUE(flatten(table.get(), &result));
+
+    Attribute* actualAttr = test::getValue<Attribute>(&result, u"@android:attr/foo");
+    ASSERT_NE(nullptr, actualAttr);
+    EXPECT_EQ(attr.isWeak(), actualAttr->isWeak());
+    EXPECT_EQ(attr.typeMask, actualAttr->typeMask);
+    EXPECT_EQ(attr.minInt, actualAttr->minInt);
+    EXPECT_EQ(attr.maxInt, actualAttr->maxInt);
+}
+
+TEST_F(TableFlattenerTest, FlattenSourceAndCommentsForChildrenOfCompoundValues) {
+    Style style;
+    Reference key(test::parseNameOrDie(u"@android:attr/foo"));
+    key.id = ResourceId(0x01010000);
+    key.setSource(Source("test").withLine(2));
+    key.setComment(StringPiece16(u"comment"));
+    style.entries.push_back(Style::Entry{ key, util::make_unique<Id>() });
+
+    test::ResourceTableBuilder builder = test::ResourceTableBuilder();
+    std::unique_ptr<ResourceTable> table = builder
+            .setPackageId(u"android", 0x01)
+            .addValue(u"@android:attr/foo", ResourceId(0x01010000),
+                      test::AttributeBuilder().build())
+            .addValue(u"@android:style/foo", ResourceId(0x01020000),
+                      std::unique_ptr<Style>(style.clone(builder.getStringPool())))
+            .build();
+
+    ResourceTable result;
+    ASSERT_TRUE(flatten(table.get(), &result));
+
+    Style* actualStyle = test::getValue<Style>(&result, u"@android:style/foo");
+    ASSERT_NE(nullptr, actualStyle);
+    ASSERT_EQ(1u, actualStyle->entries.size());
+
+    Reference* actualKey = &actualStyle->entries[0].key;
+    EXPECT_EQ(key.getSource(), actualKey->getSource());
+    EXPECT_EQ(key.getComment(), actualKey->getComment());
+}
+
 } // namespace aapt
index 4b82537..3f64d7b 100644 (file)
@@ -78,52 +78,6 @@ private:
         return value;
     }
 
-    void buildAttributeMismatchMessage(DiagMessage* msg, const Attribute* attr,
-                                       const Item* value) {
-        *msg << "expected";
-        if (attr->typeMask & android::ResTable_map::TYPE_BOOLEAN) {
-            *msg << " boolean";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_COLOR) {
-            *msg << " color";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_DIMENSION) {
-            *msg << " dimension";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_ENUM) {
-            *msg << " enum";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_FLAGS) {
-            *msg << " flags";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_FLOAT) {
-            *msg << " float";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_FRACTION) {
-            *msg << " fraction";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_INTEGER) {
-            *msg << " integer";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_REFERENCE) {
-            *msg << " reference";
-        }
-
-        if (attr->typeMask & android::ResTable_map::TYPE_STRING) {
-            *msg << " string";
-        }
-
-        *msg << " but got " << *value;
-    }
-
 public:
     using ValueVisitor::visit;
 
@@ -175,21 +129,19 @@ public:
                 entry.value->accept(this);
 
                 // Now verify that the type of this item is compatible with the attribute it
-                // is defined for.
-                android::Res_value val = {};
-                entry.value->flatten(&val);
-
-                // Always allow references.
-                const uint32_t typeMask = symbol->attribute->typeMask |
-                        android::ResTable_map::TYPE_REFERENCE;
-
-                if (!(typeMask & ResourceUtils::androidTypeToAttributeTypeMask(val.dataType))) {
+                // is defined for. We pass `nullptr` as the DiagMessage so that this check is
+                // fast and we avoid creating a DiagMessage when the match is successful.
+                if (!symbol->attribute->matches(entry.value.get(), nullptr)) {
                     // The actual type of this item is incompatible with the attribute.
                     DiagMessage msg(style->getSource());
-                    buildAttributeMismatchMessage(&msg, symbol->attribute.get(), entry.value.get());
+
+                    // Call the matches method again, this time with a DiagMessage so we fill
+                    // in the actual error message.
+                    symbol->attribute->matches(entry.value.get(), &msg);
                     mContext->getDiagnostics()->error(msg);
                     mError = true;
                 }
+
             } else {
                 DiagMessage msg(style->getSource());
                 msg << "style attribute '";
index d04181d..6ad2f9c 100644 (file)
@@ -61,7 +61,7 @@ const ISymbolTable::Symbol* SymbolTableWrapper::findByName(const ResourceName& n
         if (iter != sr.entry->values.end() && iter->config == kDefaultConfig) {
             // This resource has an Attribute.
             if (Attribute* attr = valueCast<Attribute>(iter->value.get())) {
-                symbol->attribute = std::unique_ptr<Attribute>(attr->clone(nullptr));
+                symbol->attribute = util::make_unique<Attribute>(*attr);
             } else {
                 return {};
             }
@@ -77,7 +77,6 @@ const ISymbolTable::Symbol* SymbolTableWrapper::findByName(const ResourceName& n
     return symbol.get();
 }
 
-
 static std::shared_ptr<ISymbolTable::Symbol> lookupAttributeInTable(const android::ResTable& table,
                                                                     ResourceId id) {
     // Try as a bag.
@@ -103,29 +102,40 @@ static std::shared_ptr<ISymbolTable::Symbol> lookupAttributeInTable(const androi
 
     if (s->attribute) {
         for (size_t i = 0; i < (size_t) count; i++) {
-            if (!Res_INTERNALID(entry[i].map.name.ident)) {
-                android::ResTable::resource_name entryName;
-                if (!table.getResourceName(entry[i].map.name.ident, false, &entryName)) {
-                    table.unlockBag(entry);
-                    return nullptr;
+            const android::ResTable_map& mapEntry = entry[i].map;
+            if (Res_INTERNALID(mapEntry.name.ident)) {
+                switch (mapEntry.name.ident) {
+                case android::ResTable_map::ATTR_MIN:
+                    s->attribute->minInt = static_cast<int32_t>(mapEntry.value.data);
+                    break;
+                case android::ResTable_map::ATTR_MAX:
+                    s->attribute->maxInt = static_cast<int32_t>(mapEntry.value.data);
+                    break;
                 }
+                continue;
+            }
 
-                const ResourceType* parsedType = parseResourceType(
-                        StringPiece16(entryName.type, entryName.typeLen));
-                if (!parsedType) {
-                    table.unlockBag(entry);
-                    return nullptr;
-                }
+            android::ResTable::resource_name entryName;
+            if (!table.getResourceName(mapEntry.name.ident, false, &entryName)) {
+                table.unlockBag(entry);
+                return nullptr;
+            }
 
-                Attribute::Symbol symbol;
-                symbol.symbol.name = ResourceNameRef(
-                        StringPiece16(entryName.package, entryName.packageLen),
-                        *parsedType,
-                        StringPiece16(entryName.name, entryName.nameLen)).toResourceName();
-                symbol.symbol.id = ResourceId(entry[i].map.name.ident);
-                symbol.value = entry[i].map.value.data;
-                s->attribute->symbols.push_back(std::move(symbol));
+            const ResourceType* parsedType = parseResourceType(
+                    StringPiece16(entryName.type, entryName.typeLen));
+            if (!parsedType) {
+                table.unlockBag(entry);
+                return nullptr;
             }
+
+            Attribute::Symbol symbol;
+            symbol.symbol.name = ResourceName(
+                    StringPiece16(entryName.package, entryName.packageLen),
+                    *parsedType,
+                    StringPiece16(entryName.name, entryName.nameLen));
+            symbol.symbol.id = ResourceId(mapEntry.name.ident);
+            symbol.value = mapEntry.value.data;
+            s->attribute->symbols.push_back(std::move(symbol));
         }
     }
     table.unlockBag(entry);
index 9ca694a..f8e3d03 100644 (file)
@@ -36,6 +36,10 @@ private:
 public:
     ResourceTableBuilder() = default;
 
+    StringPool* getStringPool() {
+        return &mTable->stringPool;
+    }
+
     ResourceTableBuilder& setPackageId(const StringPiece16& packageName, uint8_t id) {
         ResourceTablePackage* package = mTable->createPackage(packageName, id);
         assert(package);
index 49625b5..bd42b67 100644 (file)
@@ -771,12 +771,20 @@ std::unique_ptr<Attribute> BinaryResourceParser::parseAttr(const ResourceNameRef
         attr->typeMask = util::deviceToHost32(typeMaskIter->value.data);
     }
 
-    if (attr->typeMask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) {
-        for (const ResTable_map& mapEntry : map) {
-            if (Res_INTERNALID(util::deviceToHost32(mapEntry.name.ident))) {
-                continue;
+    for (const ResTable_map& mapEntry : map) {
+        if (Res_INTERNALID(util::deviceToHost32(mapEntry.name.ident))) {
+            switch (util::deviceToHost32(mapEntry.name.ident)) {
+            case ResTable_map::ATTR_MIN:
+                attr->minInt = static_cast<int32_t>(mapEntry.value.data);
+                break;
+            case ResTable_map::ATTR_MAX:
+                attr->maxInt = static_cast<int32_t>(mapEntry.value.data);
+                break;
             }
+            continue;
+        }
 
+        if (attr->typeMask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) {
             Attribute::Symbol symbol;
             symbol.value = util::deviceToHost32(mapEntry.value.data);
             if (util::deviceToHost32(mapEntry.name.ident) == 0) {
@@ -799,7 +807,7 @@ std::unique_ptr<Attribute> BinaryResourceParser::parseAttr(const ResourceNameRef
         }
     }
 
-    // TODO(adamlesinski): Find min, max, i80n, etc attributes.
+    // TODO(adamlesinski): Find i80n, attributes.
     return attr;
 }
 
@@ -848,22 +856,22 @@ std::unique_ptr<Plural> BinaryResourceParser::parsePlural(const ResourceNameRef&
         }
 
         switch (util::deviceToHost32(mapEntry.name.ident)) {
-            case android::ResTable_map::ATTR_ZERO:
+            case ResTable_map::ATTR_ZERO:
                 plural->values[Plural::Zero] = std::move(item);
                 break;
-            case android::ResTable_map::ATTR_ONE:
+            case ResTable_map::ATTR_ONE:
                 plural->values[Plural::One] = std::move(item);
                 break;
-            case android::ResTable_map::ATTR_TWO:
+            case ResTable_map::ATTR_TWO:
                 plural->values[Plural::Two] = std::move(item);
                 break;
-            case android::ResTable_map::ATTR_FEW:
+            case ResTable_map::ATTR_FEW:
                 plural->values[Plural::Few] = std::move(item);
                 break;
-            case android::ResTable_map::ATTR_MANY:
+            case ResTable_map::ATTR_MANY:
                 plural->values[Plural::Many] = std::move(item);
                 break;
-            case android::ResTable_map::ATTR_OTHER:
+            case ResTable_map::ATTR_OTHER:
                 plural->values[Plural::Other] = std::move(item);
                 break;
         }
index 1f7d5ce..aa409ea 100644 (file)
@@ -275,6 +275,29 @@ inline Maybe<T> make_nothing() {
     return Maybe<T>();
 }
 
+/**
+ * Define the == operator between Maybe<T> and Maybe<U> if the operator T == U is defined.
+ * Otherwise this won't be defined and the compiler will yell at the callsite instead of inside
+ * Maybe.h.
+ */
+template <typename T, typename U>
+auto operator==(const Maybe<T>& a, const Maybe<U>& b)
+-> decltype(std::declval<T> == std::declval<U>) {
+    if (a && b) {
+        return a.value() == b.value();
+    }
+    return false;
+}
+
+/**
+ * Same as operator== but negated.
+ */
+template <typename T, typename U>
+auto operator!=(const Maybe<T>& a, const Maybe<U>& b)
+-> decltype(std::declval<T> == std::declval<U>) {
+    return !(a == b);
+}
+
 } // namespace aapt
 
 #endif // AAPT_MAYBE_H
index d2c33ca..9cca40e 100644 (file)
@@ -119,4 +119,14 @@ TEST(MaybeTest, MoveAssign) {
     }
 }
 
+TEST(MaybeTest, Equality) {
+    Maybe<int> a = 1;
+    Maybe<int> b = 1;
+    Maybe<int> c;
+
+    EXPECT_EQ(a, b);
+    EXPECT_EQ(b, a);
+    EXPECT_NE(a, c);
+}
+
 } // namespace aapt