Report vendor and product information via GetDeviceProperties.

This CL adds VendorId, VendorName, ProductId, and ProductName to the
properties returned by org.chromium.CrosDisks.GetDeviceProperties. The
vendor and product information is obtained from the USB ID database on
the system.

BUG=chromium-os:33225
TEST=Tested the following:
1. Build cros-disks for {x86,amd64,arm}-generic.
2. Run cros-disks unit tests.
3. Run platform_CrosDisksDBus and platform_CrosDisksFilesystem tests.
4. Manually test mounting different removable USB devices and verify the
   vendor and product information of the devices return by
   org.chromium.CrosDisks.GetDeviceProperties.

Change-Id: I43982e6e72256a4e362beca4753caf11f2a37741
Reviewed-on: https://gerrit.chromium.org/gerrit/33044
Reviewed-by: Darin Petkov <petkov@chromium.org>
Commit-Ready: Ben Chan <benchan@chromium.org>
Tested-by: Ben Chan <benchan@chromium.org>
diff --git a/cros-disks.xml b/cros-disks.xml
index ea8cd0a..3c04698 100644
--- a/cros-disks.xml
+++ b/cros-disks.xml
@@ -13,6 +13,10 @@
       <tp:member name="DeviceIsVirtual" type="b"> </tp:member>
       <tp:member name="IdUuid" type="s"> </tp:member>
       <tp:member name="IdLabel" type="s"> </tp:member>
+      <tp:member name="VendorId" type="s"> </tp:member>
+      <tp:member name="VendorName" type="s"> </tp:member>
+      <tp:member name="ProductId" type="s"> </tp:member>
+      <tp:member name="ProductName" type="s"> </tp:member>
       <tp:member name="DeviceFile" type="s"> </tp:member>
       <tp:member name="DriveModel" type="s"> </tp:member>
       <tp:member name="DriveIsRotational" type="b"> </tp:member>
diff --git a/disk.cc b/disk.cc
index a061305..aa7e2ec 100644
--- a/disk.cc
+++ b/disk.cc
@@ -31,12 +31,6 @@
       is_rotational_(false),
       is_read_only_(false),
       is_virtual_(true),
-      mount_paths_(),
-      native_path_(),
-      device_file_(),
-      uuid_(),
-      label_(),
-      drive_model_(),
       media_type_(DEVICE_MEDIA_UNKNOWN),
       device_capacity_(0),
       bytes_remaining_(0) {
@@ -80,6 +74,10 @@
   disk[kDeviceFile].writer().append_string(device_file().c_str());
   disk[kIdUuid].writer().append_string(uuid().c_str());
   disk[kIdLabel].writer().append_string(label().c_str());
+  disk[kVendorId].writer().append_string(vendor_id().c_str());
+  disk[kVendorName].writer().append_string(vendor_name().c_str());
+  disk[kProductId].writer().append_string(product_id().c_str());
+  disk[kProductName].writer().append_string(product_name().c_str());
   disk[kDriveModel].writer().append_string(drive_model().c_str());
   disk[kDriveIsRotational].writer().append_bool(is_rotational());
   disk[kDeviceMediaType].writer().append_uint32(media_type());
diff --git a/disk.h b/disk.h
index 1f1c57d..0809aeb 100644
--- a/disk.h
+++ b/disk.h
@@ -108,6 +108,26 @@
   std::string label() const { return label_; }
   void set_label(const std::string& label) { label_ = label; }
 
+  std::string vendor_id() const { return vendor_id_; }
+  void set_vendor_id(const std::string& vendor_id) {
+    vendor_id_ = vendor_id;
+  }
+
+  std::string vendor_name() const { return vendor_name_; }
+  void set_vendor_name(const std::string& vendor_name) {
+    vendor_name_ = vendor_name;
+  }
+
+  std::string product_id() const { return product_id_; }
+  void set_product_id(const std::string& product_id) {
+    product_id_ = product_id;
+  }
+
+  std::string product_name() const { return product_name_; }
+  void set_product_name(const std::string& product_name) {
+    product_name_ = product_name;
+  }
+
   std::string drive_model() const { return drive_model_; }
   void set_drive_model(const std::string& drive_model) {
     drive_model_ = drive_model;
@@ -142,6 +162,10 @@
   std::string filesystem_type_;
   std::string uuid_;
   std::string label_;
+  std::string vendor_id_;
+  std::string vendor_name_;
+  std::string product_id_;
+  std::string product_name_;
   std::string drive_model_;
   DeviceMediaType media_type_;
   uint64 device_capacity_;
diff --git a/udev-device.cc b/udev-device.cc
index b411390..d17c969 100644
--- a/udev-device.cc
+++ b/udev-device.cc
@@ -49,6 +49,7 @@
 const char kPropertyRotationRate[] = "ID_ATA_ROTATION_RATE_RPM";
 const char kVirtualDevicePathPrefix[] = "/sys/devices/virtual/";
 const char kUSBDeviceInfoFile[] = "/opt/google/cros-disks/usb-device-info";
+const char kUSBIdentifierDatabase[] = "/usr/share/misc/usb.ids";
 const char* kNonAutoMountableFilesystemLabels[] = {
   "C-ROOT", "C-STATE", NULL
 };
@@ -193,6 +194,17 @@
   if (IsPropertyTrue(kPropertyCDROM))
     return DEVICE_MEDIA_OPTICAL_DISC;
 
+  string vendor_id, product_id;
+  if (GetVendorAndProductId(&vendor_id, &product_id)) {
+    USBDeviceInfo info;
+    info.RetrieveFromFile(kUSBDeviceInfoFile);
+    return info.GetDeviceMediaType(vendor_id, product_id);
+  }
+  return DEVICE_MEDIA_UNKNOWN;
+}
+
+bool UdevDevice::GetVendorAndProductId(
+    string* vendor_id, string* product_id) const {
   // Search up the parent device tree to obtain the vendor and product ID
   // of the first device with a device type "usb_device". Then look up the
   // media type based on the vendor and product ID from a USB device info file.
@@ -200,19 +212,18 @@
     const char *device_type =
         udev_device_get_property_value(dev, kPropertyDeviceType);
     if (device_type && strcmp(device_type, kPropertyDeviceTypeUSBDevice) == 0) {
-      const char *vendor_id =
+      const char *vendor_id_attr =
           udev_device_get_sysattr_value(dev, kAttributeIdVendor);
-      const char *product_id =
+      const char *product_id_attr =
           udev_device_get_sysattr_value(dev, kAttributeIdProduct);
-      if (vendor_id && product_id) {
-        USBDeviceInfo info;
-        info.RetrieveFromFile(kUSBDeviceInfoFile);
-        return info.GetDeviceMediaType(vendor_id, product_id);
+      if (vendor_id_attr && product_id_attr) {
+        *vendor_id = vendor_id_attr;
+        *product_id = product_id_attr;
+        return true;
       }
     }
   }
-
-  return DEVICE_MEDIA_UNKNOWN;
+  return false;
 }
 
 bool UdevDevice::IsMediaAvailable() const {
@@ -365,6 +376,21 @@
   disk.set_label(
       EnsureUTF8String(GetPropertyFromBlkId(kPropertyBlkIdFilesystemLabel)));
 
+  string vendor_id, product_id;
+  if (GetVendorAndProductId(&vendor_id, &product_id)) {
+    disk.set_vendor_id(vendor_id);
+    disk.set_product_id(product_id);
+
+    string vendor_name, product_name;
+    USBDeviceInfo info;
+    if (info.GetVendorAndProductName(kUSBIdentifierDatabase,
+                                     vendor_id, product_id,
+                                     &vendor_name, &product_name)) {
+      disk.set_vendor_name(EnsureUTF8String(vendor_name));
+      disk.set_product_name(EnsureUTF8String(product_name));
+    }
+  }
+
   const char *dev_file = udev_device_get_devnode(dev_);
   if (dev_file)
     disk.set_device_file(dev_file);
diff --git a/udev-device.h b/udev-device.h
index 0874cf3..9f80ab0 100644
--- a/udev-device.h
+++ b/udev-device.h
@@ -56,6 +56,11 @@
   // Gets the device media type used on the device.
   DeviceMediaType GetDeviceMediaType() const;
 
+  // Gets the USB vendor and product ID of the device. Returns true if the
+  // IDs are found.
+  bool GetVendorAndProductId(std::string* vendor_id,
+                             std::string* product_id) const;
+
   // Checks if a device should be auto-mounted. Currently, all external
   // disk devices, which are neither on the boot device nor virtual,
   // are considered auto-mountable.
diff --git a/usb-device-info.cc b/usb-device-info.cc
index d4cc1ed..b694aac 100644
--- a/usb-device-info.cc
+++ b/usb-device-info.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -52,8 +52,7 @@
 
   string line;
   while (reader.ReadLine(&line)) {
-    // Skip an empty or comment line.
-    if (line.empty() || StartsWithASCII(line, "#", true))
+    if (IsLineSkippable(line))
       continue;
 
     vector<string> tokens;
@@ -66,6 +65,55 @@
   return true;
 }
 
+bool USBDeviceInfo::GetVendorAndProductName(
+    const string& ids_file, const string& vendor_id, const string& product_id,
+    string* vendor_name, string* product_name) {
+  vendor_name->clear();
+  product_name->clear();
+
+  FileReader reader;
+  if (!reader.Open(FilePath(ids_file))) {
+    LOG(ERROR) << "Failed to retrieve USB identifier database at '"
+               << ids_file << "'";
+    return false;
+  }
+
+  bool found_vendor = false;
+  string line;
+  while (reader.ReadLine(&line)) {
+    if (IsLineSkippable(line))
+      continue;
+
+    string id, name;
+    // If the target vendor ID is found, search for a matching product ID.
+    if (found_vendor) {
+      if (line[0] == '\t' &&
+          ExtractIdAndName(line.substr(1), &id, &name)) {
+        if (id == product_id) {
+          *product_name = name;
+          break;
+        }
+        continue;
+      }
+
+      // If the line does not contain any product info, assume a new
+      // section has started and no product info will be found for the
+      // target ID. Return immediately.
+      break;
+    }
+
+    // Skip forward until the target vendor ID is found.
+    if (ExtractIdAndName(line, &id, &name)) {
+      if (id == vendor_id) {
+        *vendor_name = name;
+        found_vendor = true;
+      }
+    }
+  }
+
+  return found_vendor;
+}
+
 DeviceMediaType USBDeviceInfo::ConvertToDeviceMediaType(
     const string& str) const {
   if (str == "sd") {
@@ -77,4 +125,24 @@
   }
 }
 
+bool USBDeviceInfo::IsLineSkippable(const string& line) const {
+  string trimmed_line;
+  // Trim only ASCII whitespace for now.
+  TrimWhitespaceASCII(line, TRIM_ALL, &trimmed_line);
+  return trimmed_line.empty() || StartsWithASCII(trimmed_line, "#", true);
+}
+
+bool USBDeviceInfo::ExtractIdAndName(
+    const string& line, string* id, string* name) const {
+  if ((line.length() > 6) &&
+      IsHexDigit(line[0]) && IsHexDigit(line[1]) &&
+      IsHexDigit(line[2]) && IsHexDigit(line[3]) &&
+      (line[4] == ' ') && (line[5] == ' ')) {
+    *id = StringToLowerASCII(line.substr(0, 4));
+    *name = line.substr(6);
+    return true;
+  }
+  return false;
+}
+
 }  // namespace cros_disks
diff --git a/usb-device-info.h b/usb-device-info.h
index dd1c4ac..07f998c 100644
--- a/usb-device-info.h
+++ b/usb-device-info.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -31,15 +31,35 @@
   // Returns true on success.
   bool RetrieveFromFile(const std::string& path);
 
+  // Gets the vendor and product name that correspond to |vendor_id| and
+  // |product_id| from a USB ID database at |ids_file|.
+  bool GetVendorAndProductName(const std::string& ids_file,
+                               const std::string& vendor_id,
+                               const std::string& product_id,
+                               std::string* vendor_name,
+                               std::string* product_name);
+
  private:
   // Converts from string to enum of a device media type.
   DeviceMediaType ConvertToDeviceMediaType(const std::string& str) const;
 
+  // Returns true if |line| contains a 4-digit hex identifier and a name
+  // separated by two spaces, i.e. "<4-digit hex ID>  <descriptive name>".
+  // The extracted identifier and name are returned via |id| and |name|,
+  // respectively.
+  bool ExtractIdAndName(const std::string& line,
+                        std::string* id, std::string* name) const;
+
+  // Returns true if |line| is skippable, i.e. an empty or comment line.
+  bool IsLineSkippable(const std::string& line) const;
+
   // A map from an ID string, in form of <vendor id>:<product id>, to a
   // USBDeviceEntry struct.
   std::map<std::string, USBDeviceEntry> entries_;
 
   FRIEND_TEST(USBDeviceInfoTest, ConvertToDeviceMediaType);
+  FRIEND_TEST(USBDeviceInfoTest, ExtractIdAndName);
+  FRIEND_TEST(USBDeviceInfoTest, IsLineSkippable);
 
   DISALLOW_COPY_AND_ASSIGN(USBDeviceInfo);
 };
diff --git a/usb-device-info_unittest.cc b/usb-device-info_unittest.cc
index 8ea1f7c..e5476bc 100644
--- a/usb-device-info_unittest.cc
+++ b/usb-device-info_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -14,26 +14,61 @@
 class USBDeviceInfoTest : public ::testing::Test {
  public:
   virtual void SetUp() {
-    string content =
-      "# This is a comment line\n"
-      " \n"
-      "\n"
-      "18d1:4e11 mobile\n"
-      "0bda:0138 sd\n";
+    info_file_ = CreateTestDataFile(
+        "# This is a comment line\n"
+        " \n"
+        "\n"
+        "18d1:4e11 mobile\n"
+        "0bda:0138 sd\n");
+    ASSERT_FALSE(info_file_.empty());
 
-    FilePath info_file;
-    ASSERT_TRUE(file_util::CreateTemporaryFile(&info_file));
-    ASSERT_EQ(content.size(),
-              file_util::WriteFile(info_file, content.c_str(), content.size()));
-    info_file_ = info_file.value();
+    ids_file_ = CreateTestDataFile(
+        "#\n"
+        "#\tList of USB ID's\n"
+        "#\n"
+        "\n"
+        "# Syntax:\n"
+        "# vendor  vendor_name\n"
+        "\tdevice  device_name\n"
+        "#\t\tinterface  interface_name\n"
+        "  \n"
+        "0123  Vendor A\n"
+        "\tab01  Product 1\n"
+        "\tab02  Product 2\n"
+        "\tab03  Product 3\n"
+        "  \n"
+        "5678  Vendor with no product IDs\n"
+        "abcd  Vendor B\n"
+        "\t0004  Product X\n"
+        "\t\n"
+        "  \n"
+        "# comment\n"
+        "\t0005  Product Y\n"
+        "\t0006  Product Z\n"
+        "\n"
+        "C 00  Class 0\n");
+    ASSERT_FALSE(ids_file_.empty());
   }
 
   virtual void TearDown() {
     ASSERT_TRUE(file_util::Delete(FilePath(info_file_), false));
+    ASSERT_TRUE(file_util::Delete(FilePath(ids_file_), false));
   }
 
  protected:
+  string CreateTestDataFile(const string& content) const {
+    FilePath temp_file;
+    if (file_util::CreateTemporaryFile(&temp_file) &&
+        (static_cast<size_t>(file_util::WriteFile(temp_file, content.c_str(),
+                                                  content.size())) ==
+         content.size())) {
+      return temp_file.value();
+    }
+    return string();
+  }
+
   string info_file_;
+  string ids_file_;
   USBDeviceInfo info_;
 };
 
@@ -50,6 +85,35 @@
   EXPECT_TRUE(info_.RetrieveFromFile(info_file_));
 }
 
+TEST_F(USBDeviceInfoTest, GetVendorAndProductName) {
+  string vendor_name, product_name;
+
+  EXPECT_FALSE(info_.GetVendorAndProductName(
+      "nonexistent-path", "0123", "ab01", &vendor_name, &product_name));
+  EXPECT_FALSE(info_.GetVendorAndProductName(
+      ids_file_, "1234", "ab01", &vendor_name, &product_name));
+
+  EXPECT_TRUE(info_.GetVendorAndProductName(
+      ids_file_, "0123", "0000", &vendor_name, &product_name));
+  EXPECT_EQ("Vendor A", vendor_name);
+  EXPECT_EQ("", product_name);
+
+  EXPECT_TRUE(info_.GetVendorAndProductName(
+      ids_file_, "0123", "ab03", &vendor_name, &product_name));
+  EXPECT_EQ("Vendor A", vendor_name);
+  EXPECT_EQ("Product 3", product_name);
+
+  EXPECT_TRUE(info_.GetVendorAndProductName(
+      ids_file_, "5678", "0005", &vendor_name, &product_name));
+  EXPECT_EQ("Vendor with no product IDs", vendor_name);
+  EXPECT_EQ("", product_name);
+
+  EXPECT_TRUE(info_.GetVendorAndProductName(
+      ids_file_, "abcd", "0005", &vendor_name, &product_name));
+  EXPECT_EQ("Vendor B", vendor_name);
+  EXPECT_EQ("Product Y", product_name);
+}
+
 TEST_F(USBDeviceInfoTest, ConvertToDeviceMediaType) {
   EXPECT_EQ(DEVICE_MEDIA_MOBILE, info_.ConvertToDeviceMediaType("mobile"));
   EXPECT_EQ(DEVICE_MEDIA_SD, info_.ConvertToDeviceMediaType("sd"));
@@ -58,4 +122,37 @@
   EXPECT_EQ(DEVICE_MEDIA_USB, info_.ConvertToDeviceMediaType("foo"));
 }
 
+TEST_F(USBDeviceInfoTest, ExtractIdAndName) {
+  string id, name;
+  EXPECT_FALSE(info_.ExtractIdAndName("", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("0123  ", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("012  test device", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("0123 test device", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("x123  test device", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("0x23  test device", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("01x3  test device", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("012x  test device", &id, &name));
+  EXPECT_FALSE(info_.ExtractIdAndName("01234 test device", &id, &name));
+
+  EXPECT_TRUE(info_.ExtractIdAndName("0123  test device", &id, &name));
+  EXPECT_EQ("0123", id);
+  EXPECT_EQ("test device", name);
+
+  EXPECT_TRUE(info_.ExtractIdAndName("ABCD  T", &id, &name));
+  EXPECT_EQ("abcd", id);
+  EXPECT_EQ("T", name);
+}
+
+TEST_F(USBDeviceInfoTest, IsLineSkippable) {
+  EXPECT_TRUE(info_.IsLineSkippable(""));
+  EXPECT_TRUE(info_.IsLineSkippable("  "));
+  EXPECT_TRUE(info_.IsLineSkippable("\t"));
+  EXPECT_TRUE(info_.IsLineSkippable("#"));
+  EXPECT_TRUE(info_.IsLineSkippable("# this is a comment"));
+  EXPECT_TRUE(info_.IsLineSkippable(" # this is a comment"));
+  EXPECT_TRUE(info_.IsLineSkippable("# this is a comment "));
+  EXPECT_TRUE(info_.IsLineSkippable("\t#this is a comment"));
+  EXPECT_FALSE(info_.IsLineSkippable("this is not a comment"));
+}
+
 }  // namespace cros_disks