soma: Initial spec reading and parsing code

The SpecReader class has methods for reading a parsing a
container specification into a ContainerSpec object. Among
other data, this ContainerSpec holds lists of 'Filter'
objects that express which device and sysfs nodes should
be visible inside the specified container.

BUG=brillo:304
TEST=unit tests

Change-Id: If73669791ce402da03085550b9010550a38f6043
Reviewed-on: https://chromium-review.googlesource.com/254484
Reviewed-by: Chris Masone <cmasone@chromium.org>
Commit-Queue: Chris Masone <cmasone@chromium.org>
Tested-by: Chris Masone <cmasone@chromium.org>
diff --git a/soma/README b/soma/README
index b3498f3..b6f8c15 100644
--- a/soma/README
+++ b/soma/README
@@ -34,11 +34,12 @@
 
   struct ContainerSpec {
     String ServiceBundlePath;
-    Int DesiredUser;
-    Int DesiredGroup;
+    Int DesiredUserID;
+    Int DesiredGroupID;
 
     List<Int> ListeningPorts;
-    List<DeviceFilter> DeviceFilters;
+    List<DeviceNodeFilter> DeviceNodeFilters;
+    List<DevicePathFilter> DevicePathFilters;
     List<SysfsPathFilter> SysfsPathFilters;
     List<UsbDeviceFilter> UsbDeviceFilters;
   };
@@ -50,7 +51,8 @@
 
  Optional fields:
   ListeningPorts: List of network ports on which the service can listen.
-  DeviceFilters: Filters indicating which device nodes should be visible.
+  DeviceNodeFilters: Filters indicating which device nodes should be visible.
+  DevicePathFilters: Filters indicating which device paths should be visible.
   SysfsPathFilters: Filters indicating which sysfs paths should be visible.
   UsbDeviceFilters; Filters indicating which USB devices should be visible.
 
@@ -58,14 +60,12 @@
 Filter types - Several kinds of filters used to control access to pieces of
                hardware at runtime.
 
-  struct DeviceFilter {};
-
-  struct DeviceNodeFilter : DeviceFilter {
+  struct DeviceNodeFilter {
     Int major;		May be a wildcard
     Int minor;		May be a wildcard
   };
 
-  struct DevicePathFilter : DeviceFilter {
+  struct DevicePathFilter {
     String filter;	May contain globs (language TBD)
   };
 
diff --git a/soma/container_spec.cc b/soma/container_spec.cc
new file mode 100644
index 0000000..1b02212
--- /dev/null
+++ b/soma/container_spec.cc
@@ -0,0 +1,70 @@
+// Copyright 2015 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.
+
+#include "soma/container_spec.h"
+
+#include <sys/types.h>
+
+#include <algorithm>
+#include <set>
+#include <string>
+
+#include <base/files/file_path.h>
+#include <base/memory/scoped_vector.h>
+
+#include "soma/device_filter.h"
+#include "soma/sysfs_filter.h"
+#include "soma/usb_device_filter.h"
+
+namespace soma {
+
+ContainerSpec::ContainerSpec(const base::FilePath& service_bundle_path,
+                             uid_t uid,
+                             gid_t gid)
+    : service_bundle_path_(service_bundle_path),
+      uid_(uid),
+      gid_(gid) {
+}
+
+ContainerSpec::~ContainerSpec() {}
+
+void ContainerSpec::AddListenPort(int port) {
+  listen_ports_.insert(port);
+}
+
+void ContainerSpec::AddDevicePathFilter(const std::string& filter) {
+  device_path_filters_.push_back(new DevicePathFilter(base::FilePath(filter)));
+}
+
+void ContainerSpec::AddDeviceNodeFilter(int major, int minor) {
+  device_node_filters_.push_back(new DeviceNodeFilter(major, minor));
+}
+
+void ContainerSpec::AddSysfsFilter(const std::string& filter) {
+  sysfs_filters_.push_back(new SysfsFilter(base::FilePath(filter)));
+}
+
+void ContainerSpec::AddUSBDeviceFilter(int vid, int pid) {
+  usb_device_filters_.push_back(new USBDeviceFilter(vid, pid));
+}
+
+bool ContainerSpec::DevicePathIsAllowed(const base::FilePath& query) {
+  return
+      device_path_filters_.end() !=
+      std::find_if(device_path_filters_.begin(), device_path_filters_.end(),
+                   [query](DevicePathFilter* to_check) {
+                     return to_check->Allows(query);
+                   });
+}
+
+bool ContainerSpec::DeviceNodeIsAllowed(int major, int minor) {
+  return
+      device_node_filters_.end() !=
+      std::find_if(device_node_filters_.begin(), device_node_filters_.end(),
+                   [major, minor](DeviceNodeFilter* to_check) {
+                     return to_check->Allows(major, minor);
+                   });
+}
+
+}  // namespace soma
diff --git a/soma/container_spec.h b/soma/container_spec.h
new file mode 100644
index 0000000..98bafc8
--- /dev/null
+++ b/soma/container_spec.h
@@ -0,0 +1,67 @@
+// Copyright 2015 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.
+
+#ifndef SOMA_CONTAINER_SPEC_H_
+#define SOMA_CONTAINER_SPEC_H_
+
+#include <sys/types.h>
+
+#include <set>
+#include <string>
+#include <vector>
+
+#include <base/files/file_path.h>
+#include <base/memory/scoped_vector.h>
+
+#include "soma/device_filter.h"
+#include "soma/sysfs_filter.h"
+#include "soma/usb_device_filter.h"
+
+namespace soma {
+
+// Holds intermediate representation of container specification.
+// TODO(cmasone): Serialization of this will need to be a thing.
+class ContainerSpec {
+ public:
+  ContainerSpec(const base::FilePath& service_bundle_path,
+                uid_t uid,
+                gid_t gid);
+  virtual ~ContainerSpec();
+
+  void AddListenPort(int port);
+
+  void AddDevicePathFilter(const std::string& filter);
+  void AddDeviceNodeFilter(int major, int minor);
+  void AddSysfsFilter(const std::string& filter);
+  void AddUSBDeviceFilter(int vid, int pid);
+
+  const base::FilePath& service_bundle_path() { return service_bundle_path_; }
+  uid_t uid() { return uid_; }
+  gid_t gid() { return gid_; }
+
+  // Returns true if there's a DevicePathFilter that matches query.
+  bool DevicePathIsAllowed(const base::FilePath& query);
+
+  // Returns true if there's a DeviceNodeFilter that matches major and minor.
+  bool DeviceNodeIsAllowed(int major, int minor);
+
+ private:
+  const base::FilePath service_bundle_path_;
+  const uid_t uid_;
+  const gid_t gid_;
+
+  std::set<int> listen_ports_;
+
+  // TODO(cmasone): As we gain more experience with these, investigate whether
+  // they should also be sets, or at leat have set semantics.
+  ScopedVector<DevicePathFilter> device_path_filters_;
+  ScopedVector<DeviceNodeFilter> device_node_filters_;
+  ScopedVector<SysfsFilter> sysfs_filters_;
+  ScopedVector<USBDeviceFilter> usb_device_filters_;
+
+  DISALLOW_COPY_AND_ASSIGN(ContainerSpec);
+};
+
+}  // namespace soma
+#endif  // SOMA_CONTAINER_SPEC_H_
diff --git a/soma/container_spec_unittest.cc b/soma/container_spec_unittest.cc
new file mode 100644
index 0000000..0c2618a
--- /dev/null
+++ b/soma/container_spec_unittest.cc
@@ -0,0 +1,50 @@
+// Copyright 2015 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.
+
+#include "soma/container_spec.h"
+
+#include <string>
+
+#include <base/files/file_path.h>
+#include <base/files/file_util.h>
+#include <base/files/scoped_temp_dir.h>
+#include <base/logging.h>
+#include <gtest/gtest.h>
+
+namespace soma {
+
+class ContainerSpecTest : public ::testing::Test {
+ public:
+  ContainerSpecTest() {}
+  virtual ~ContainerSpecTest() {}
+};
+
+TEST_F(ContainerSpecTest, DevicePathFilterTest) {
+  base::ScopedTempDir tmpdir;
+  base::FilePath scratch;
+  ASSERT_TRUE(tmpdir.CreateUniqueTempDir());
+  ASSERT_TRUE(base::CreateTemporaryFileInDir(tmpdir.path(), &scratch));
+
+  ContainerSpec spec(base::FilePath("/foo/bar"), 0, 0);
+  std::string device_path("/dev/thing");
+  spec.AddDevicePathFilter(device_path);
+
+  EXPECT_TRUE(spec.DevicePathIsAllowed(base::FilePath(device_path)));
+  EXPECT_FALSE(spec.DevicePathIsAllowed(base::FilePath("/not/a/thing")));
+}
+
+TEST_F(ContainerSpecTest, DeviceNodeFilterTest) {
+  base::ScopedTempDir tmpdir;
+  base::FilePath scratch;
+  ASSERT_TRUE(tmpdir.CreateUniqueTempDir());
+  ASSERT_TRUE(base::CreateTemporaryFileInDir(tmpdir.path(), &scratch));
+
+  ContainerSpec spec(base::FilePath("/foo/bar"), 0, 0);
+  spec.AddDeviceNodeFilter(1, 2);
+
+  EXPECT_TRUE(spec.DeviceNodeIsAllowed(1, 2));
+  EXPECT_FALSE(spec.DeviceNodeIsAllowed(0, 1));
+}
+
+}  // namespace soma
diff --git a/soma/device_filter.cc b/soma/device_filter.cc
new file mode 100644
index 0000000..b848d06
--- /dev/null
+++ b/soma/device_filter.cc
@@ -0,0 +1,22 @@
+// Copyright 2015 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.
+
+#include "soma/device_filter.h"
+
+#include <base/files/file_path.h>
+
+namespace soma {
+
+DevicePathFilter::DevicePathFilter(const base::FilePath& path) : filter_(path) {
+}
+
+DevicePathFilter::~DevicePathFilter() {}
+
+DeviceNodeFilter::DeviceNodeFilter(int major, int minor)
+    : major_(major), minor_(minor) {
+}
+
+DeviceNodeFilter::~DeviceNodeFilter() {}
+
+}  // namespace soma
diff --git a/soma/device_filter.h b/soma/device_filter.h
new file mode 100644
index 0000000..1d9e352
--- /dev/null
+++ b/soma/device_filter.h
@@ -0,0 +1,63 @@
+// Copyright 2015 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.
+
+#ifndef SOMA_DEVICE_FILTER_H_
+#define SOMA_DEVICE_FILTER_H_
+
+#include <base/files/file_path.h>
+
+namespace soma {
+
+class DevicePathFilter {
+ public:
+  // Will be useful if I put these in a std::set<>, which I might.
+  using Comparator = bool(*)(const DevicePathFilter&, const DevicePathFilter&);
+  static bool Comp(const DevicePathFilter& a, const DevicePathFilter& b) {
+    return a.Precedes(b);
+  }
+
+  explicit DevicePathFilter(const base::FilePath& path);
+  virtual ~DevicePathFilter();
+
+  bool Precedes(const DevicePathFilter& rhs) const {
+    return filter_.value() < rhs.filter_.value();
+  }
+
+  bool Allows(const base::FilePath& rhs) const {
+    return filter_.value() == rhs.value();
+  }
+
+ private:
+  const base::FilePath filter_;
+  DISALLOW_COPY_AND_ASSIGN(DevicePathFilter);
+};
+
+class DeviceNodeFilter{
+ public:
+  // Will be useful if I put these in a std::set<>, which I might.
+  using Comparator = bool(*)(const DeviceNodeFilter&, const DeviceNodeFilter&);
+  static bool Comp(const DeviceNodeFilter& a, const DeviceNodeFilter& b) {
+    return a.Precedes(b);
+  }
+
+  DeviceNodeFilter(int major, int minor);
+  virtual ~DeviceNodeFilter();
+
+  bool Precedes(const DeviceNodeFilter& rhs) const {
+    return major_ < rhs.major_ || (major_ == rhs.major_ && minor_ < rhs.minor_);
+  }
+
+  // TODO(cmasone): handle wildcarding in both major and minor.
+  bool Allows(int major, int minor) const {
+    return major_ == major && minor_ == minor;
+  }
+
+ private:
+  int major_;
+  int minor_;
+  DISALLOW_COPY_AND_ASSIGN(DeviceNodeFilter);
+};
+
+}  // namespace soma
+#endif  // SOMA_DEVICE_FILTER_H_
diff --git a/soma/soma.gyp b/soma/soma.gyp
index 0df8a14..41784b1 100644
--- a/soma/soma.gyp
+++ b/soma/soma.gyp
@@ -3,13 +3,23 @@
     'defines': [
       '__STDC_FORMAT_MACROS',
     ],
+    'variables': {
+      'deps': [
+        'libchrome-<(libbase_ver)',
+      ],
+    },
   },
   'targets': [
     {
       'target_name': 'libsoma',
       'type': 'static_library',
       'sources': [
+        'container_spec.cc',
+        'device_filter.cc',
+        'spec_reader.cc',
+        'sysfs_filter.cc',
         'soma.cc',
+        'usb_device_filter.cc',
       ],
     },
   ],
@@ -23,7 +33,9 @@
           'defines': ['UNIT_TEST'],
           'dependencies': ['libsoma'],
           'sources': [
+            'container_spec_unittest.cc',
             'soma_testrunner.cc',
+            'spec_reader_unittest.cc',
           ],
         },
       ],
diff --git a/soma/spec_reader.cc b/soma/spec_reader.cc
new file mode 100644
index 0000000..82ee70a
--- /dev/null
+++ b/soma/spec_reader.cc
@@ -0,0 +1,116 @@
+// Copyright 2015 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.
+
+#include "soma/spec_reader.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <base/files/file_path.h>
+#include <base/files/file_util.h>
+#include <base/json/json_reader.h>
+#include <base/memory/scoped_ptr.h>
+#include <base/values.h>
+
+#include "soma/container_spec.h"
+
+namespace soma {
+namespace {
+// Helper function that parses a list of integer pairs.
+std::vector<std::pair<int, int>> ParseIntegerPairs(base::ListValue* filters) {
+  std::vector<std::pair<int, int>> to_return;
+  for (base::Value* filter : *filters) {
+    base::ListValue* nested = nullptr;
+    if (!(filter->GetAsList(&nested) && nested->GetSize() == 2)) {
+      LOG(ERROR) << "Device node filter must be a list of 2 elements.";
+      continue;
+    }
+    int major, minor;
+    if (!nested->GetInteger(0, &major) || !nested->GetInteger(1, &minor)) {
+      LOG(ERROR) << "Device node filter must contain 2 ints.";
+      continue;
+    }
+    to_return.push_back(std::make_pair(major, minor));
+  }
+  return to_return;
+}
+}  // anonymous namespace
+
+const char ContainerSpecReader::kServiceBundlePathKey[] = "service bundle path";
+const char ContainerSpecReader::kUidKey[] = "uid";
+const char ContainerSpecReader::kGidKey[] = "gid";
+
+const char ContainerSpecReader::kDevicePathFiltersKey[] = "device path filters";
+const char ContainerSpecReader::kDeviceNodeFiltersKey[] = "device node filters";
+
+ContainerSpecReader::ContainerSpecReader()
+    : reader_(base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS) {
+}
+
+ContainerSpecReader::~ContainerSpecReader() {
+}
+
+scoped_ptr<ContainerSpec> ContainerSpecReader::Read(
+    const base::FilePath& spec_file) {
+  LOG(INFO) << "Reading container spec at " << spec_file.value();
+  std::string spec_string;
+  if (!base::ReadFileToString(spec_file, &spec_string)) {
+    PLOG(ERROR) << "Can't read " << spec_file.value();
+    return nullptr;
+  }
+  return Parse(spec_string);
+}
+
+scoped_ptr<ContainerSpec> ContainerSpecReader::Parse(const std::string& json) {
+  scoped_ptr<base::Value> root = make_scoped_ptr(reader_.ReadToValue(json));
+  if (!root) {
+    LOG(ERROR) << "Failed to parse: " << reader_.GetErrorMessage();
+    return nullptr;
+  }
+  base::DictionaryValue* spec_dict = nullptr;
+  if (!root->GetAsDictionary(&spec_dict)) {
+    LOG(ERROR) << "Spec should have been a dictionary.";
+    return nullptr;
+  }
+
+  std::string service_bundle_path;
+  if (!spec_dict->GetString(kServiceBundlePathKey, &service_bundle_path)) {
+    LOG(ERROR) << "service bundle path is required.";
+    return nullptr;
+  }
+
+  int uid, gid;
+  if (!spec_dict->GetInteger(kUidKey, &uid) ||
+      !spec_dict->GetInteger(kGidKey, &gid)) {
+    LOG(ERROR) << "uid and gid are required.";
+    return nullptr;
+  }
+
+  scoped_ptr<ContainerSpec> spec(
+      new ContainerSpec(base::FilePath(service_bundle_path), uid, gid));
+
+  base::ListValue* device_path_filters = nullptr;
+  std::string temp_filter_string;
+  if (spec_dict->GetList(kDevicePathFiltersKey, &device_path_filters)) {
+    for (base::Value* filter : *device_path_filters) {
+      if (!filter->GetAsString(&temp_filter_string)) {
+        LOG(ERROR) << "Device path filters must be strings.";
+        continue;
+      }
+      spec->AddDevicePathFilter(temp_filter_string);
+    }
+  }
+
+  base::ListValue* device_node_filters = nullptr;
+  if (spec_dict->GetList(kDeviceNodeFiltersKey, &device_node_filters)) {
+    for (const auto& num_pair : ParseIntegerPairs(device_node_filters)) {
+      spec->AddDeviceNodeFilter(num_pair.first, num_pair.second);
+    }
+  }
+
+  return spec.Pass();
+}
+
+}  // namespace soma
diff --git a/soma/spec_reader.h b/soma/spec_reader.h
new file mode 100644
index 0000000..1018570
--- /dev/null
+++ b/soma/spec_reader.h
@@ -0,0 +1,51 @@
+// Copyright 2015 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.
+
+#ifndef SOMA_SPEC_READER_H_
+#define SOMA_SPEC_READER_H_
+
+#include <string>
+
+#include <base/files/file_path.h>
+#include <base/json/json_reader.h>
+#include <base/memory/scoped_ptr.h>
+
+namespace base {
+class ListValue;
+}
+
+namespace soma {
+class ContainerSpec;
+
+// A class that handles reading a container specification written in JSON
+// from disk and parsing it into a ContainerSpec object.
+class ContainerSpecReader {
+ public:
+  // Keys for required fields in a container specification.
+  static const char kServiceBundlePathKey[];
+  static const char kUidKey[];
+  static const char kGidKey[];
+
+  // Keys for optional fields in a container specification.
+  static const char kDevicePathFiltersKey[];
+  static const char kDeviceNodeFiltersKey[];
+
+  ContainerSpecReader();
+  virtual ~ContainerSpecReader();
+
+  // Read a container specification at spec_file and return a ContainerSpec
+  // object. Returns nullptr on failures and logs appropriate messages.
+  scoped_ptr<ContainerSpec> Read(const base::FilePath& spec_file);
+
+ private:
+  // Workhorse for doing the parsing of specific fields in the spec.
+  scoped_ptr<ContainerSpec> Parse(const std::string& json);
+
+  base::JSONReader reader_;
+
+  DISALLOW_COPY_AND_ASSIGN(ContainerSpecReader);
+};
+}  // namespace soma
+
+#endif  // SOMA_SPEC_READER_H_
diff --git a/soma/spec_reader_unittest.cc b/soma/spec_reader_unittest.cc
new file mode 100644
index 0000000..4f42633
--- /dev/null
+++ b/soma/spec_reader_unittest.cc
@@ -0,0 +1,117 @@
+// Copyright 2015 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.
+
+#include "soma/spec_reader.h"
+
+#include <string>
+#include <utility>
+
+#include <base/files/file_path.h>
+#include <base/files/file_util.h>
+#include <base/files/scoped_temp_dir.h>
+#include <base/json/json_reader.h>
+#include <base/json/json_writer.h>
+#include <base/logging.h>
+#include <base/values.h>
+#include <gtest/gtest.h>
+
+#include "soma/container_spec.h"
+
+namespace soma {
+
+class ContainerSpecReaderTest : public ::testing::Test {
+ public:
+  ContainerSpecReaderTest() {}
+  virtual ~ContainerSpecReaderTest() {}
+
+  void SetUp() override {
+    ASSERT_TRUE(tmpdir_.CreateUniqueTempDir());
+    ASSERT_TRUE(base::CreateTemporaryFileInDir(tmpdir_.path(), &scratch_));
+  }
+
+ protected:
+  scoped_ptr<base::DictionaryValue> BuildBaselineValue() {
+    scoped_ptr<base::DictionaryValue> baseline_spec(new base::DictionaryValue);
+    baseline_spec->SetString(ContainerSpecReader::kServiceBundlePathKey,
+                             kServiceBundlePath);
+    baseline_spec->SetInteger(ContainerSpecReader::kUidKey, kUid);
+    baseline_spec->SetInteger(ContainerSpecReader::kGidKey, kGid);
+    return baseline_spec.Pass();
+  }
+
+  void WriteValue(base::Value* to_write, const base::FilePath& file) {
+    std::string value_string;
+    ASSERT_TRUE(base::JSONWriter::Write(to_write, &value_string));
+    ASSERT_EQ(
+        base::WriteFile(file, value_string.c_str(), value_string.length()),
+        value_string.length());
+  }
+
+  void CheckSpecBaseline(ContainerSpec* spec) {
+    ASSERT_TRUE(spec);
+    ASSERT_EQ(spec->service_bundle_path(),
+              base::FilePath(kServiceBundlePath));
+    ASSERT_EQ(spec->uid(), kUid);
+    ASSERT_EQ(spec->gid(), kGid);
+  }
+
+  ContainerSpecReader reader_;
+  base::ScopedTempDir tmpdir_;
+  base::FilePath scratch_;
+
+ private:
+  static const char kServiceBundlePath[];
+  static const uid_t kUid;
+  static const gid_t kGid;
+};
+
+const char ContainerSpecReaderTest::kServiceBundlePath[] = "/services/bundle";
+const uid_t ContainerSpecReaderTest::kUid = 1;
+const gid_t ContainerSpecReaderTest::kGid = 2;
+
+TEST_F(ContainerSpecReaderTest, BaselineSpec) {
+  scoped_ptr<base::DictionaryValue> baseline = BuildBaselineValue();
+  WriteValue(baseline.get(), scratch_);
+
+  scoped_ptr<ContainerSpec> spec = reader_.Read(scratch_);
+  CheckSpecBaseline(spec.get());
+}
+
+namespace {
+scoped_ptr<base::ListValue> ListFromPair(const std::pair<int, int>& pair) {
+  scoped_ptr<base::ListValue> list(new base::ListValue);
+  list->AppendInteger(pair.first);
+  list->AppendInteger(pair.second);
+  return list.Pass();
+}
+}  // anonymous namespace
+
+TEST_F(ContainerSpecReaderTest, SpecWithDeviceFilters) {
+  scoped_ptr<base::DictionaryValue> baseline = BuildBaselineValue();
+
+  const char kPathFilter1[] = "/dev/d1";
+  const char kPathFilter2[] = "/dev/d2";
+  scoped_ptr<base::ListValue> device_path_filters(new base::ListValue);
+  device_path_filters->AppendString(kPathFilter1);
+  device_path_filters->AppendString(kPathFilter2);
+  baseline->Set(ContainerSpecReader::kDevicePathFiltersKey,
+                device_path_filters.release());
+
+  scoped_ptr<base::ListValue> device_node_filters(new base::ListValue);
+  device_node_filters->Append(ListFromPair(std::make_pair(8, 0)).release());
+  device_node_filters->Append(ListFromPair(std::make_pair(4, -1)).release());
+  baseline->Set(ContainerSpecReader::kDeviceNodeFiltersKey,
+                device_node_filters.release());
+
+  WriteValue(baseline.get(), scratch_);
+
+  scoped_ptr<ContainerSpec> spec = reader_.Read(scratch_);
+  CheckSpecBaseline(spec.get());
+  EXPECT_TRUE(spec->DevicePathIsAllowed(base::FilePath(kPathFilter1)));
+  EXPECT_TRUE(spec->DevicePathIsAllowed(base::FilePath(kPathFilter2)));
+  EXPECT_TRUE(spec->DeviceNodeIsAllowed(8, 0));
+  EXPECT_TRUE(spec->DeviceNodeIsAllowed(4, -1));
+}
+
+}  // namespace soma
diff --git a/soma/sysfs_filter.cc b/soma/sysfs_filter.cc
new file mode 100644
index 0000000..86f5aa1
--- /dev/null
+++ b/soma/sysfs_filter.cc
@@ -0,0 +1,16 @@
+// Copyright 2015 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.
+
+#include "soma/sysfs_filter.h"
+
+#include <base/files/file_path.h>
+
+namespace soma {
+
+SysfsFilter::SysfsFilter(const base::FilePath& path) : filter_(path) {
+}
+
+SysfsFilter::~SysfsFilter() {}
+
+}  // namespace soma
diff --git a/soma/sysfs_filter.h b/soma/sysfs_filter.h
new file mode 100644
index 0000000..026a132
--- /dev/null
+++ b/soma/sysfs_filter.h
@@ -0,0 +1,23 @@
+// Copyright 2015 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.
+
+#ifndef SOMA_SYSFS_FILTER_H_
+#define SOMA_SYSFS_FILTER_H_
+
+#include <base/files/file_path.h>
+
+namespace soma {
+
+class SysfsFilter {
+ public:
+  explicit SysfsFilter(const base::FilePath& path);
+  virtual ~SysfsFilter();
+
+ private:
+  const base::FilePath filter_;
+  DISALLOW_COPY_AND_ASSIGN(SysfsFilter);
+};
+
+}  // namespace soma
+#endif  // SOMA_SYSFS_FILTER_H_
diff --git a/soma/usb_device_filter.cc b/soma/usb_device_filter.cc
new file mode 100644
index 0000000..ecaef2c
--- /dev/null
+++ b/soma/usb_device_filter.cc
@@ -0,0 +1,15 @@
+// Copyright 2015 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.
+
+#include "soma/usb_device_filter.h"
+
+namespace soma {
+
+USBDeviceFilter::~USBDeviceFilter() {}
+
+USBDeviceFilter::USBDeviceFilter(int vid, int pid)
+    : vid_(vid), pid_(pid) {
+}
+
+}  // namespace soma
diff --git a/soma/usb_device_filter.h b/soma/usb_device_filter.h
new file mode 100644
index 0000000..2153151
--- /dev/null
+++ b/soma/usb_device_filter.h
@@ -0,0 +1,29 @@
+// Copyright 2015 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.
+
+#ifndef SOMA_USB_DEVICE_FILTER_H_
+#define SOMA_USB_DEVICE_FILTER_H_
+
+#include <base/basictypes.h>
+
+namespace soma {
+
+class USBDeviceFilter {
+ public:
+  USBDeviceFilter(int vid, int pid);
+  virtual ~USBDeviceFilter();
+
+  // TODO(cmasone): handle wildcarding in both major and minor.
+  bool Allows(const USBDeviceFilter& rhs) {
+    return vid_ == rhs.vid_ && pid_ == rhs.pid_;
+  }
+
+ private:
+  int vid_;
+  int pid_;
+  DISALLOW_COPY_AND_ASSIGN(USBDeviceFilter);
+};
+
+}  // namespace soma
+#endif  // SOMA_USB_DEVICE_FILTER_H_