Add support for Bazel

When generating Dart proto in Bazel, source protos are located relative
to their Bazel package and the `.pb.dart` output is emitted to an
equivalent path under the lib dir. e.g., `//a/b/c:d/e/f.proto` will
generate an output at `//a/b/c:lib/d/e/f.pb.dart`.
diff --git a/bin/protoc_plugin_bazel.dart b/bin/protoc_plugin_bazel.dart
new file mode 100755
index 0000000..4ab1b79
--- /dev/null
+++ b/bin/protoc_plugin_bazel.dart
@@ -0,0 +1,15 @@
+#!/usr/bin/env dart
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+import 'package:protoc_plugin/bazel.dart';
+import 'package:protoc_plugin/protoc.dart';
+
+void main() {
+  var packages = {};
+  new CodeGenerator(stdin, stdout).generate(
+      optionParsers: {bazelOptionId: new BazelOptionParser(packages)},
+      config: new BazelOutputConfiguration(packages));
+}
diff --git a/lib/bazel.dart b/lib/bazel.dart
new file mode 100644
index 0000000..6bfe90a
--- /dev/null
+++ b/lib/bazel.dart
@@ -0,0 +1,146 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Bazel support for protoc_plugin.
+library protoc_bazel;
+
+import 'package:path/path.dart' as p;
+import 'protoc.dart' show SingleOptionParser, DefaultOutputConfiguration;
+
+/// Dart protoc plugin option for Bazel packages.
+///
+/// This option takes a semicolon-separated list of Bazel package metadata in
+/// `package_name|input_root|output_root` form. `input_root` designates the
+/// directory relative to which input protos are located -- typically the root
+/// of the Bazel package, where the `BUILD` file is located. `output_root`
+/// designates the directory relative to which generated `.pb.dart` outputs are
+/// emitted -- typically the package's `lib/` directory under the genfiles
+/// directory specified by `genfiles_dir` in the Bazel configuration. Generated
+/// outputs are emitted at the same path relative to `output_root` as the input
+/// proto is found relative to `input_root`.
+///
+/// For example, using `foo.bar|foo/bar|foo/bar/lib`:
+///   * `foo/bar/baz.proto` will generate `foo/bar/lib/baz.pb.dart`
+///   * `foo/bar/a/b/baz.proto` will generate `foo/bar/lib/a/b/baz.pb.dart`
+const bazelOptionId = 'BazelPackages';
+
+class BazelPackage {
+  final String name;
+  final String input_root;
+  final String output_root;
+
+  BazelPackage(this.name, String input_root, String output_root)
+      : input_root = p.normalize(input_root),
+        output_root = p.normalize(output_root);
+}
+
+/// Parser for the `BazelPackages` option.
+class BazelOptionParser implements SingleOptionParser {
+  /// Output map of package input_root to package.
+  final Map<String, BazelPackage> output;
+
+  BazelOptionParser(this.output);
+
+  @override
+  void parse(String name, String value, onError(String message)) {
+    if (value == null) {
+      onError('Invalid $bazelOptionId option. Expected a non-empty value.');
+      return;
+    }
+
+    for (var entry in value.split(';')) {
+      var fields = entry.split('|');
+      if (fields.length != 3) {
+        onError(
+            'ERROR: expected package_name|input_root|output_root. Got: $entry');
+        continue;
+      }
+      var pkg = new BazelPackage(fields[0], fields[1], fields[2]);
+      if (!output.containsKey(pkg.input_root)) {
+        output[pkg.input_root] = pkg;
+      } else {
+        var prev = output[pkg.input_root];
+        if (pkg.name != prev.name) {
+          onError('ERROR: multiple packages with input_root ${pkg.input_root}: '
+              '${prev.name} and ${pkg.name}');
+          continue;
+        }
+        if (pkg.output_root != prev.output_root) {
+          onError('ERROR: conflicting output_roots for package ${pkg.name}: '
+              '${prev.output_root} and ${pkg.output_root}');
+          continue;
+        }
+      }
+    }
+  }
+}
+
+/// A Dart `package:` URI with package name and path components.
+class _PackageUri {
+  final String packageName;
+  final String path;
+  Uri get uri => Uri.parse('package:$packageName/$path');
+
+  _PackageUri(this.packageName, this.path);
+}
+
+/// [OutputConfiguration] that uses Bazel layout information to resolve output
+/// locations and imports.
+class BazelOutputConfiguration extends DefaultOutputConfiguration {
+  final Map<String, BazelPackage> packages;
+
+  BazelOutputConfiguration(this.packages);
+
+  /// Search for the most specific Bazel package above [searchPath].
+  BazelPackage _findPackage(String searchPath) {
+    var index = searchPath.lastIndexOf('/');
+    while (index > 0) {
+      searchPath = searchPath.substring(0, index);
+      var pkg = packages[searchPath];
+      if (pkg != null) return pkg;
+      index = searchPath.lastIndexOf('/');
+    }
+    return null;
+  }
+
+  @override
+  Uri outputPathFor(Uri input) {
+    var pkg = _findPackage(input.path);
+    if (pkg == null) {
+      throw new ArgumentError('Unable to locate package for input $input.');
+    }
+
+    // Bazel package-relative paths.
+    var relativeInput = input.path.substring('${pkg.input_root}/'.length);
+    var relativeOutput = replacePathExtension(relativeInput);
+    var outputPath = p.join(pkg.output_root, relativeOutput);
+    return new Uri.file(outputPath);
+  }
+
+  @override
+  Uri resolveImport(Uri target, Uri source) {
+    var targetUri = _packageUriFor(replacePathExtension(target.path));
+    var sourceUri = _packageUriFor(source.path);
+
+    if (targetUri == null && sourceUri != null) {
+      // We can't reach outside of the lib/ directory of a package without
+      // using a package: import. Using a relative import for [target] could
+      // break anyone who uses a package: import to load [source].
+      throw 'ERROR: cannot generate import for $target from $source.';
+    }
+
+    if (targetUri != null && sourceUri?.packageName != targetUri.packageName) {
+      return targetUri.uri;
+    }
+
+    return super.resolveImport(target, source);
+  }
+
+  _PackageUri _packageUriFor(String target) {
+    var pkg = _findPackage(target);
+    if (pkg == null) return null;
+    var relPath = target.substring(pkg.input_root.length + 1);
+    return new _PackageUri(pkg.name, relPath);
+  }
+}
diff --git a/test/all_tests.dart b/test/all_tests.dart
index f4b2b52..c0e1e9d 100755
--- a/test/all_tests.dart
+++ b/test/all_tests.dart
@@ -5,6 +5,7 @@
 
 library protoc_plugin_all_tests;
 
+import 'bazel_test.dart' as bazel;
 import 'client_generator_test.dart' as client_generator;
 import 'const_generator_test.dart' as const_generator;
 import 'enum_generator_test.dart' as enum_generator;
@@ -26,6 +27,7 @@
 import 'wire_format_test.dart' as wire_format;
 
 void main() {
+  bazel.main();
   client_generator.main();
   const_generator.main();
   enum_generator.main();
diff --git a/test/bazel_test.dart b/test/bazel_test.dart
new file mode 100644
index 0000000..af28b49
--- /dev/null
+++ b/test/bazel_test.dart
@@ -0,0 +1,199 @@
+// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library bazel_test;
+
+import 'package:protoc_plugin/bazel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('BazelOptionParser', () {
+    var optionParser;
+    var packages;
+    var errors;
+
+    setUp(() {
+      packages = {};
+      optionParser = new BazelOptionParser(packages);
+      errors = [];
+    });
+
+    _onError(String message) {
+      errors.add(message);
+    }
+
+    test('should call onError for null values', () {
+      optionParser.parse(null, null, _onError);
+      expect(errors, isNotEmpty);
+    });
+
+    test('should call onError for empty values', () {
+      optionParser.parse(null, '', _onError);
+      expect(errors, isNotEmpty);
+    });
+
+    test('should call onError for malformed entries', () {
+      optionParser.parse(null, 'foo', _onError);
+      optionParser.parse(null, 'foo|bar', _onError);
+      optionParser.parse(null, 'foo|bar|baz|quux', _onError);
+      expect(errors.length, 3);
+      expect(packages, isEmpty);
+    });
+
+    test('should handle a single package|path entry', () {
+      optionParser.parse(null, 'foo|bar/baz|wibble/wobble', _onError);
+      expect(errors, isEmpty);
+      expect(packages.length, 1);
+      expect(packages['bar/baz'].name, 'foo');
+      expect(packages['bar/baz'].input_root, 'bar/baz');
+      expect(packages['bar/baz'].output_root, 'wibble/wobble');
+    });
+
+    test('should handle multiple package|path entries', () {
+      optionParser.parse(
+          null,
+          'foo|bar/baz|wibble/wobble;a|b/c/d|e/f;one.two|three|four/five',
+          _onError);
+      expect(errors, isEmpty);
+      expect(packages.length, 3);
+      expect(packages['bar/baz'].name, 'foo');
+      expect(packages['bar/baz'].input_root, 'bar/baz');
+      expect(packages['bar/baz'].output_root, 'wibble/wobble');
+      expect(packages['b/c/d'].name, 'a');
+      expect(packages['b/c/d'].input_root, 'b/c/d');
+      expect(packages['b/c/d'].output_root, 'e/f');
+      expect(packages['three'].name, 'one.two');
+      expect(packages['three'].input_root, 'three');
+      expect(packages['three'].output_root, 'four/five');
+    });
+
+    test('should skip and continue past malformed entries', () {
+      optionParser.parse(null,
+          'foo|bar/baz|wibble/wobble;fizz;a.b|c/d|e/f;x|y|zz|y', _onError);
+      expect(errors.length, 2);
+      expect(packages.length, 2);
+      expect(packages['bar/baz'].name, 'foo');
+      expect(packages['c/d'].name, 'a.b');
+    });
+
+    test('should emit error for conflicting package names', () {
+      optionParser.parse(null,
+          'foo|bar/baz|wibble/wobble;flob|bar/baz|wibble/wobble', _onError);
+      expect(errors.length, 1);
+      expect(packages.length, 1);
+      expect(packages['bar/baz'].name, 'foo');
+    });
+
+    test('should emit error for conflicting output_roots', () {
+      optionParser.parse(null,
+          'foo|bar/baz|wibble/wobble;foo|bar/baz|womble/wumble', _onError);
+      expect(errors.length, 1);
+      expect(packages.length, 1);
+      expect(packages['bar/baz'].output_root, 'wibble/wobble');
+    });
+
+    test('should normalize paths', () {
+      optionParser.parse(
+          null, 'foo|bar//baz/|quux/;a|b/|c;c|d//e/f///|g//h//', _onError);
+      expect(errors, isEmpty);
+      expect(packages.length, 3);
+      expect(packages['bar/baz'].name, 'foo');
+      expect(packages['bar/baz'].input_root, 'bar/baz');
+      expect(packages['bar/baz'].output_root, 'quux');
+      expect(packages['b'].name, 'a');
+      expect(packages['b'].input_root, 'b');
+      expect(packages['b'].output_root, 'c');
+      expect(packages['d/e/f'].name, 'c');
+      expect(packages['d/e/f'].input_root, 'd/e/f');
+      expect(packages['d/e/f'].output_root, 'g/h');
+    });
+  });
+
+  group('BazelOutputConfiguration', () {
+    var packages;
+    var config;
+
+    setUp(() {
+      packages = {
+        'foo/bar': new BazelPackage('a.b.c', 'foo/bar', 'baz/flob'),
+        'foo/bar/baz': new BazelPackage('d.e.f', 'foo/bar/baz', 'baz/flob/foo'),
+        'wibble/wobble':
+            new BazelPackage('wibble.wobble', 'wibble/wobble', 'womble/wumble'),
+      };
+      config = new BazelOutputConfiguration(packages);
+    });
+
+    group('outputPathForUri', () {
+      test('should handle files at package root', () {
+        var p = config.outputPathFor(Uri.parse('foo/bar/quux.proto'));
+        expect(p.path, 'baz/flob/quux.pb.dart');
+      });
+
+      test('should handle files below package root', () {
+        var p = config.outputPathFor(Uri.parse('foo/bar/a/b/quux.proto'));
+        expect(p.path, 'baz/flob/a/b/quux.pb.dart');
+      });
+
+      test('should handle files in a nested package root', () {
+        var p = config.outputPathFor(Uri.parse('foo/bar/baz/quux.proto'));
+        expect(p.path, 'baz/flob/foo/quux.pb.dart');
+      });
+
+      test('should handle files below a nested package root', () {
+        var p = config.outputPathFor(Uri.parse('foo/bar/baz/a/b/quux.proto'));
+        expect(p.path, 'baz/flob/foo/a/b/quux.pb.dart');
+      });
+
+      test('should throw if unable to locate the package for an input', () {
+        expect(
+            () => config.outputPathFor(Uri.parse('a/b/c/quux.proto')), throws);
+      });
+    });
+
+    group('resolveImport', () {
+      test('should emit relative import if in same package', () {
+        var target = Uri.parse('foo/bar/quux.proto');
+        var source = Uri.parse('foo/bar/baz.proto');
+        var uri = config.resolveImport(target, source);
+        expect(uri.path, 'quux.pb.dart');
+      });
+
+      test('should emit relative import if in subdir of same package', () {
+        var target = Uri.parse('foo/bar/a/b/quux.proto');
+        var source = Uri.parse('foo/bar/baz.proto');
+        var uri = config.resolveImport(target, source);
+        expect(uri.path, 'a/b/quux.pb.dart');
+      });
+
+      test('should emit relative import if in parent dir in same package', () {
+        var target = Uri.parse('foo/bar/quux.proto');
+        var source = Uri.parse('foo/bar/a/b/baz.proto');
+        var uri = config.resolveImport(target, source);
+        expect(uri.path, '../../quux.pb.dart');
+      });
+
+      test('should emit package: import if in different package', () {
+        var target = Uri.parse('wibble/wobble/quux.proto');
+        var source = Uri.parse('foo/bar/baz.proto');
+        var uri = config.resolveImport(target, source);
+        expect(uri.scheme, 'package');
+        expect(uri.path, 'wibble.wobble/quux.pb.dart');
+      });
+
+      test('should emit package: import if in subdir of different package', () {
+        var target = Uri.parse('wibble/wobble/foo/bar/quux.proto');
+        var source = Uri.parse('foo/bar/baz.proto');
+        var uri = config.resolveImport(target, source);
+        expect(uri.scheme, 'package');
+        expect(uri.path, 'wibble.wobble/foo/bar/quux.pb.dart');
+      });
+
+      test('should throw if target is in unknown package', () {
+        var target = Uri.parse('flob/flub/quux.proto');
+        var source = Uri.parse('foo/bar/baz.proto');
+        expect(() => config.resolveImport(target, source), throws);
+      });
+    });
+  });
+}