Add crates_updater, similar to git*_updater

* This will check/update packages from crates.io

Test: updater.sh check rust/crates/bitflags
Test: updater.sh update rust/crates/<some_package>
Change-Id: I279b47baac0583e25545f41b7646a6fae742f63c
diff --git a/Android.bp b/Android.bp
index c4f158d..77feab8 100644
--- a/Android.bp
+++ b/Android.bp
@@ -36,6 +36,7 @@
     name: "external_updater_lib",
     srcs: [
         "archive_utils.py",
+        "crates_updater.py",
         "fileutils.py",
         "git_updater.py",
         "git_utils.py",
diff --git a/archive_utils.py b/archive_utils.py
index f0198c5..fe9934a 100644
--- a/archive_utils.py
+++ b/archive_utils.py
@@ -89,6 +89,10 @@
     for ext, func in ARCHIVE_TYPES.items():
         if filename.endswith(ext):
             return func
+    # crates.io download url does not have file suffix
+    # e.g., https://crates.io/api/v1/crates/syn/1.0.16/download
+    if url.find('/crates.io/api/') > 0:
+        return untar
     return None
 
 
diff --git a/crates_updater.py b/crates_updater.py
new file mode 100644
index 0000000..9458c62
--- /dev/null
+++ b/crates_updater.py
@@ -0,0 +1,90 @@
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Module to check updates from crates.io."""
+
+
+import json
+import re
+import urllib.request
+
+import archive_utils
+import fileutils
+import metadata_pb2    # pylint: disable=import-error
+import updater_utils
+
+
+CRATES_IO_URL_PATTERN = (r'^https:\/\/crates.io\/crates\/([-\w]+)')
+
+CRATES_IO_URL_RE = re.compile(CRATES_IO_URL_PATTERN)
+
+
+class CratesUpdater():
+    """Updater for crates.io packages."""
+
+    def __init__(self, url, proj_path, metadata):
+        if url.type != metadata_pb2.URL.HOMEPAGE:
+            raise ValueError('Only check HOMEPAGE url.')
+        match = CRATES_IO_URL_RE.match(url.value)
+        if match is None:
+            raise ValueError('HOMEPAGE url must have crates.io.')
+        self.proj_path = proj_path
+        self.metadata = metadata
+        self.package = match.group(1)
+        self.upstream_url = url
+        self.new_version = None
+        self.dl_path = None
+
+    def check(self):
+        """Checks crates.io and returns whether a new version is available."""
+        url = 'https://crates.io/api/v1/crates/{}/versions'.format(self.package)
+        with urllib.request.urlopen(url) as request:
+            data = json.loads(request.read().decode())
+        versions = data['versions']
+        # version with the largest id number is assumed to be the latest
+        last_id = 0
+        for v in versions:
+            if int(v['id']) > last_id:
+                last_id = int(v['id'])
+                self.new_version = v['num']
+                self.dl_path = v['dl_path']
+        print('Current version: {}. Latest version: {}'.format(
+            self.get_current_version(), self.new_version), end='')
+
+    def get_current_version(self):
+        """Returns the latest version name recorded in METADATA."""
+        return self.metadata.third_party.version
+
+    def get_latest_version(self):
+        """Returns the latest version name in upstream."""
+        return self.new_version
+
+    def _write_metadata(self, path):
+        updated_metadata = metadata_pb2.MetaData()
+        updated_metadata.CopyFrom(self.metadata)
+        updated_metadata.third_party.version = self.new_version
+        fileutils.write_metadata(path, updated_metadata)
+
+    def update(self):
+        """Updates the package.
+
+        Has to call check() before this function.
+        """
+        try:
+            url = 'https://crates.io' + self.dl_path
+            temporary_dir = archive_utils.download_and_extract(url)
+            package_dir = archive_utils.find_archive_root(temporary_dir)
+            self._write_metadata(package_dir)
+            updater_utils.replace_package(package_dir, self.proj_path)
+        finally:
+            urllib.request.urlcleanup()
diff --git a/external_updater.py b/external_updater.py
index 1745549..2af66cd 100644
--- a/external_updater.py
+++ b/external_updater.py
@@ -27,6 +27,7 @@
 
 from google.protobuf import text_format    # pylint: disable=import-error
 
+from crates_updater import CratesUpdater
 from git_updater import GitUpdater
 from github_archive_updater import GithubArchiveUpdater
 import fileutils
@@ -34,7 +35,7 @@
 import updater_utils
 
 
-UPDATERS = [GithubArchiveUpdater, GitUpdater]
+UPDATERS = [CratesUpdater, GithubArchiveUpdater, GitUpdater]
 
 USE_COLOR = sys.stdout.isatty()
 
diff --git a/update_package.sh b/update_package.sh
index d7cfa42..1cf7cc2 100644
--- a/update_package.sh
+++ b/update_package.sh
@@ -39,6 +39,7 @@
 CopyIfPresent "NOTICE"
 cp -a -f -n $external_dir/MODULE_LICENSE_* .
 CopyIfPresent "METADATA"
+CopyIfPresent "TEST_MAPPING"
 CopyIfPresent ".git"
 CopyIfPresent ".gitignore"
 CopyIfPresent "patches"
@@ -63,4 +64,7 @@
 rm -rf $external_dir
 mv $tmp_dir $external_dir
 
+cd $external_dir
+git add .
+
 exit 0