mojo/syncbase: Implementing naming escape and unescape
in Dart.

encodeAsNameElement makes a string representable as
a name element by escaping slashes.

decodeAsNameElement decodes an encoded name element.

also some reorganization to separate integration and
unit tests now that we have unit tests.

Change-Id: I7d266a8a895e5556096d68c8177202ef237f696c
diff --git a/Makefile b/Makefile
index e19bbef..ff8e70e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 PWD := $(shell pwd)
 DART_FILES := $(shell find dart/bin dart/lib dart/test sky_demo/lib -name "*.dart" -not -path "dart/lib/gen/*")
-GO_FILES := $(shell find go/src -name "*.go")
+GO_FILES := $(shell find gen/go/src -name "*.go")
 V23_GO_FILES := $(shell find $(V23_ROOT) -name "*.go")
 
 include ../shared/mojo.mk
@@ -131,7 +131,14 @@
 	$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/sky_demo/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) 'mojo:window_manager https://mojo.v.io/sky_demo/lib/main.dart'
 
 .PHONY: test
-test: dart/packages $(ETHER_BUILD_DIR)/syncbase_server.mojo gen-mojom | syncbase-env-check
+test: test-unit test-integration
+
+.PHONY: test-unit
+test-unit: dart/packages
+	cd dart && pub run test test/unit
+
+.PHONY: test-integration
+test-integration: dart/packages $(ETHER_BUILD_DIR)/syncbase_server.mojo gen-mojom | syncbase-env-check
 	$(MOJO_DIR)/src/mojo/devtools/common/mojo_test --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) --shell-path $(MOJO_SHELL_PATH) tests
 
 .PHONY: syncbase-env-check
diff --git a/README.md b/README.md
index e21f655..ecaae0e 100644
--- a/README.md
+++ b/README.md
@@ -91,7 +91,7 @@
 The following command will run a single test file.  This is useful when the
 full test suite hangs with no output.
 
-    $(MOJO_DIR)/src/mojo/devtools/common/mojo_run -v --enable-multiprocess --shell-path $(MOJO_DIR)/src/out/Debug/mojo_shell dart/test/<filename>
+    $(MOJO_DIR)/src/mojo/devtools/common/mojo_run -v --enable-multiprocess --shell-path $(MOJO_DIR)/src/out/Debug/mojo_shell dart/test/integration/<filename>
 
 [architecture proposal]: https://docs.google.com/document/d/1TyxPYIhj9VBCtY7eAXu_MEV9y0dtRx7n7UY4jm76Qq4/edit
 [depot tools]: http://www.chromium.org/developers/how-tos/install-depot-tools
diff --git a/dart/lib/src/naming/util.dart b/dart/lib/src/naming/util.dart
new file mode 100644
index 0000000..2d14018
--- /dev/null
+++ b/dart/lib/src/naming/util.dart
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. 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:convert' show UTF8;
+
+// TODO(aghassemi): Move these naming utilities outside of Syncbase when we have
+// a Dart Vanadium library
+
+// Makes a string representable as a name element by escaping slashes.
+String encodeAsNameElement(String name) {
+  return escape(name, '/');
+}
+
+// Decodes an encoded name element.
+// It throws exception when encountering wrongly encoded names.
+// Note that this is more than the inverse of EncodeAsNameElement since it
+// can handle more hex encodings than / and %.
+// This is intentional since we'll most likely want to add other letters to
+// the set to be encoded.
+String decodeAsNameElement(String name) {
+  return unescape(name);
+}
+
+// Encodes a string replacing the characters in <special> and % with a
+// %<hex> escape.
+String escape(String text, String special) {
+  const String encodedPecent = '%25';
+  const String percent = '%';
+
+  // Replace % with %25 first.
+  var encodedText = text.replaceAll(percent, encodedPecent);
+
+  // For all characters in special, replace occurrences with their hex encoding.
+  // Note that we want to support any Unicode character in special, single-byte
+  // or not. For example a three-byte 읔 character, becomes %EC%9D%94.
+  for (var i = 0; i < special.length; i++) {
+    var char = special[i];
+    if (char == '%') {
+      // Ignore % in special, we have already escaped %.
+      continue;
+    }
+    var bytes = UTF8.encoder.convert(char);
+    var hex = '';
+    for (var byte in bytes) {
+      hex += percent;
+      hex += _byteToHex(byte);
+    }
+    encodedText = encodedText.replaceAll(char, hex);
+  }
+
+  return encodedText;
+}
+
+// Decodes %<hex> encodings in a string into the relevant character.
+// It throws exception when encountering wrongly encoded text.
+String unescape(String text) {
+  // Note that this function is a slightly modified version of _uriDecode() code
+  // in Dart sdk/lib/core/uri.dart (https://goo.gl/1ppJIj).
+  // The biggest difference is that, unlike _uriDecode(), our code DOES NOT
+  // expect non-ASCII characters to have been percent encoded in text.
+  // Dart's _uriDecode() however expects character above 127 to have been
+  // percent encoded or it will throw an argument exception when it encounters a
+  // character above 127.
+  const int percent = 0x25;
+
+  // First check whether there are any characters which need special handling.
+  bool notEncoded = true;
+  for (int i = 0; i < text.length && notEncoded; i++) {
+    var codeUnit = text.codeUnitAt(i);
+    notEncoded = codeUnit != percent;
+  }
+  if (notEncoded) {
+    return text;
+  }
+  List<int> bytes = new List();
+  for (int i = 0; i < text.length; i++) {
+    var codeUnit = text.codeUnitAt(i);
+    if (codeUnit == percent) {
+      if (i + 3 > text.length) {
+        throw new ArgumentError('Trucated or malformed encoded string');
+      }
+      bytes.add(_hexCharPairToByte(text, i + 1));
+      i += 2;
+    } else {
+      bytes.addAll(UTF8.encoder.convert(text[i]));
+    }
+  }
+  return UTF8.decode(bytes);
+}
+
+// Converts a byte to 0 padded hex string.
+// Note that this function is the same as byteToHex() code
+// in Dart sdk/lib/core/uri.dart (https://goo.gl/1ppJIj)
+String _byteToHex(int byte) {
+  const String hex = '0123456789ABCDEF';
+  var buffer = new StringBuffer();
+  buffer.writeCharCode(hex.codeUnitAt(byte >> 4));
+  buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f));
+  return buffer.toString();
+}
+
+// Converts a hex string to a byte.
+// Note that this function is the same code as _hexCharPairToByte()
+// in Dart sdk/lib/core/uri.dart (https://goo.gl/1ppJIj)
+int _hexCharPairToByte(String s, int pos) {
+  int byte = 0;
+  for (int i = 0; i < 2; i++) {
+    var charCode = s.codeUnitAt(pos + i);
+    if (0x30 <= charCode && charCode <= 0x39) {
+      byte = byte * 16 + charCode - 0x30;
+    } else {
+      // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66).
+      charCode |= 0x20;
+      if (0x61 <= charCode && charCode <= 0x66) {
+        byte = byte * 16 + charCode - 0x57;
+      } else {
+        throw new ArgumentError("Trucated or malformed encoded string");
+      }
+    }
+  }
+  return byte;
+}
diff --git a/dart/test/syncbase_app_test.dart b/dart/test/integration/syncbase_app_test.dart
similarity index 100%
rename from dart/test/syncbase_app_test.dart
rename to dart/test/integration/syncbase_app_test.dart
diff --git a/dart/test/syncbase_database_test.dart b/dart/test/integration/syncbase_database_test.dart
similarity index 93%
rename from dart/test/syncbase_database_test.dart
rename to dart/test/integration/syncbase_database_test.dart
index 8e7b697..279d3fc 100644
--- a/dart/test/syncbase_database_test.dart
+++ b/dart/test/integration/syncbase_database_test.dart
@@ -16,7 +16,7 @@
     var dbName = utils.uniqueName('db');
     var db = app.noSqlDatabase(dbName);
     expect(db.relativeName, equals(dbName));
-    expect(db.fullName, equals(app.fullName + '/' + dbName));
+    expect(db.fullName, equals(app.fullName + '/\$/' + dbName));
   });
 
   test('creating and destroying a database', () async {
diff --git a/dart/test/syncbase_row_test.dart b/dart/test/integration/syncbase_row_test.dart
similarity index 95%
rename from dart/test/syncbase_row_test.dart
rename to dart/test/integration/syncbase_row_test.dart
index 7397f2c..3b32141 100644
--- a/dart/test/syncbase_row_test.dart
+++ b/dart/test/integration/syncbase_row_test.dart
@@ -22,7 +22,7 @@
     var row = table.row(rowName);
 
     expect(row.relativeName, equals(rowName));
-    expect(row.fullName, equals(table.fullName + '/' + rowName));
+    expect(row.fullName, equals(table.fullName + '/\$/' + rowName));
   });
 
   test('putting, getting and deleting row', () async {
diff --git a/dart/test/syncbase_table_test.dart b/dart/test/integration/syncbase_table_test.dart
similarity index 98%
rename from dart/test/syncbase_table_test.dart
rename to dart/test/integration/syncbase_table_test.dart
index a590885..de8b6d0 100644
--- a/dart/test/syncbase_table_test.dart
+++ b/dart/test/integration/syncbase_table_test.dart
@@ -19,7 +19,7 @@
     var tableName = utils.uniqueName('table');
     var table = db.table(tableName);
     expect(table.relativeName, equals(tableName));
-    expect(table.fullName, equals(db.fullName + '/' + tableName));
+    expect(table.fullName, equals(db.fullName + '/\$/' + tableName));
   });
 
   test('creating and destroying a table', () async {
diff --git a/dart/test/syncbase_test.dart b/dart/test/integration/syncbase_test.dart
similarity index 100%
rename from dart/test/syncbase_test.dart
rename to dart/test/integration/syncbase_test.dart
diff --git a/dart/test/utils.dart b/dart/test/integration/utils.dart
similarity index 100%
rename from dart/test/utils.dart
rename to dart/test/integration/utils.dart
diff --git a/dart/test/unit/naming_util_test.dart b/dart/test/unit/naming_util_test.dart
new file mode 100644
index 0000000..e946912
--- /dev/null
+++ b/dart/test/unit/naming_util_test.dart
@@ -0,0 +1,46 @@
+library naming_util_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/src/naming/util.dart' as naming;
+
+main() {
+  var tests = [
+    ['', ''],
+    ['/', '%2F'],
+    ['%', '%25'],
+    ['/The % rain in /% Spain', '%2FThe %25 rain in %2F%25 Spain'],
+    ['/%/%', '%2F%25%2F%25'],
+    ['ᚸӲ읔+קAل', 'ᚸӲ읔+קAل'],
+    ['ᚸ/Ӳ%읔/ק%A+ل', 'ᚸ%2FӲ%25읔%2Fק%25A+ل'],
+  ];
+
+  test('encoding name elements', () {
+    for (var t in tests) {
+      expect(naming.encodeAsNameElement(t[0]), equals(t[1]));
+    }
+  });
+
+  test('decoding name elements encoded by our encoder', () {
+    for (var t in tests) {
+      expect(naming.decodeAsNameElement(t[1]), equals(t[0]));
+    }
+  });
+
+  test('decoding name elements encoded by third-party percent encoder', () {
+    for (var t in tests) {
+      var uriEncoded = Uri.encodeComponent(t[0]);
+      expect(naming.decodeAsNameElement(uriEncoded), equals(t[0]));
+    }
+  });
+
+  test('escape and unescape with multiple special characters', () {
+    var test = 'ᚸ/Ӳ%읔/ק%A+ل';
+    // Escape multiple characters, also % in special should be ignored since
+    // % is always escaped regardless.
+    var escaped = naming.escape(test, '/읔%');
+    expect(escaped, isNot(contains('읔')));
+    expect(escaped, contains('Ӳ'));
+    expect(naming.unescape(escaped), equals(test));
+  });
+}
diff --git a/tests b/tests
index 170b9be..02e7a9c 100644
--- a/tests
+++ b/tests
@@ -2,7 +2,7 @@
 
 tests = [
 	{
-		"test": "https://test.mojo.v.io/syncbase_test.dart",
+		"test": "https://test.mojo.v.io/integration/syncbase_test.dart",
 		"type": "dart",
 		"timeout": 30
 	}