• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/drive/drive_api_util.h"
6 
7 #include <string>
8 
9 #include "base/command_line.h"
10 #include "base/files/scoped_platform_file_closer.h"
11 #include "base/logging.h"
12 #include "base/md5.h"
13 #include "base/platform_file.h"
14 #include "base/strings/string16.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/values.h"
19 #include "chrome/browser/drive/drive_switches.h"
20 #include "content/public/browser/browser_thread.h"
21 #include "google_apis/drive/drive_api_parser.h"
22 #include "google_apis/drive/gdata_wapi_parser.h"
23 #include "net/base/escape.h"
24 #include "third_party/re2/re2/re2.h"
25 #include "url/gurl.h"
26 
27 namespace drive {
28 namespace util {
29 namespace {
30 
31 // Google Apps MIME types:
32 const char kGoogleDocumentMimeType[] = "application/vnd.google-apps.document";
33 const char kGoogleDrawingMimeType[] = "application/vnd.google-apps.drawing";
34 const char kGooglePresentationMimeType[] =
35     "application/vnd.google-apps.presentation";
36 const char kGoogleSpreadsheetMimeType[] =
37     "application/vnd.google-apps.spreadsheet";
38 const char kGoogleTableMimeType[] = "application/vnd.google-apps.table";
39 const char kGoogleFormMimeType[] = "application/vnd.google-apps.form";
40 const char kDriveFolderMimeType[] = "application/vnd.google-apps.folder";
41 
CopyScopedVectorString(const ScopedVector<std::string> & source)42 ScopedVector<std::string> CopyScopedVectorString(
43     const ScopedVector<std::string>& source) {
44   ScopedVector<std::string> result;
45   result.reserve(source.size());
46   for (size_t i = 0; i < source.size(); ++i)
47     result.push_back(new std::string(*source[i]));
48 
49   return result.Pass();
50 }
51 
52 // Converts AppIcon (of GData WAPI) to DriveAppIcon.
53 scoped_ptr<google_apis::DriveAppIcon>
ConvertAppIconToDriveAppIcon(const google_apis::AppIcon & app_icon)54 ConvertAppIconToDriveAppIcon(const google_apis::AppIcon& app_icon) {
55   scoped_ptr<google_apis::DriveAppIcon> resource(
56       new google_apis::DriveAppIcon);
57   switch (app_icon.category()) {
58     case google_apis::AppIcon::ICON_UNKNOWN:
59       resource->set_category(google_apis::DriveAppIcon::UNKNOWN);
60       break;
61     case google_apis::AppIcon::ICON_DOCUMENT:
62       resource->set_category(google_apis::DriveAppIcon::DOCUMENT);
63       break;
64     case google_apis::AppIcon::ICON_APPLICATION:
65       resource->set_category(google_apis::DriveAppIcon::APPLICATION);
66       break;
67     case google_apis::AppIcon::ICON_SHARED_DOCUMENT:
68       resource->set_category(google_apis::DriveAppIcon::SHARED_DOCUMENT);
69       break;
70     default:
71       NOTREACHED();
72   }
73 
74   resource->set_icon_side_length(app_icon.icon_side_length());
75   resource->set_icon_url(app_icon.GetIconURL());
76   return resource.Pass();
77 }
78 
79 // Converts InstalledApp to AppResource.
80 scoped_ptr<google_apis::AppResource>
ConvertInstalledAppToAppResource(const google_apis::InstalledApp & installed_app)81 ConvertInstalledAppToAppResource(
82     const google_apis::InstalledApp& installed_app) {
83   scoped_ptr<google_apis::AppResource> resource(new google_apis::AppResource);
84   resource->set_application_id(installed_app.app_id());
85   resource->set_name(installed_app.app_name());
86   resource->set_object_type(installed_app.object_type());
87   resource->set_supports_create(installed_app.supports_create());
88   resource->set_product_url(installed_app.GetProductUrl());
89 
90   {
91     ScopedVector<std::string> primary_mimetypes(
92         CopyScopedVectorString(installed_app.primary_mimetypes()));
93     resource->set_primary_mimetypes(primary_mimetypes.Pass());
94   }
95   {
96     ScopedVector<std::string> secondary_mimetypes(
97         CopyScopedVectorString(installed_app.secondary_mimetypes()));
98     resource->set_secondary_mimetypes(secondary_mimetypes.Pass());
99   }
100   {
101     ScopedVector<std::string> primary_file_extensions(
102         CopyScopedVectorString(installed_app.primary_extensions()));
103     resource->set_primary_file_extensions(primary_file_extensions.Pass());
104   }
105   {
106     ScopedVector<std::string> secondary_file_extensions(
107         CopyScopedVectorString(installed_app.secondary_extensions()));
108     resource->set_secondary_file_extensions(secondary_file_extensions.Pass());
109   }
110 
111   {
112     const ScopedVector<google_apis::AppIcon>& app_icons =
113         installed_app.app_icons();
114     ScopedVector<google_apis::DriveAppIcon> icons;
115     icons.reserve(app_icons.size());
116     for (size_t i = 0; i < app_icons.size(); ++i) {
117       icons.push_back(ConvertAppIconToDriveAppIcon(*app_icons[i]).release());
118     }
119     resource->set_icons(icons.Pass());
120   }
121 
122   // supports_import, installed and authorized are not supported in
123   // InstalledApp.
124 
125   return resource.Pass();
126 }
127 
128 // Returns the argument string.
Identity(const std::string & resource_id)129 std::string Identity(const std::string& resource_id) { return resource_id; }
130 
131 }  // namespace
132 
133 
IsDriveV2ApiEnabled()134 bool IsDriveV2ApiEnabled() {
135   const CommandLine* command_line = CommandLine::ForCurrentProcess();
136 
137   // Enable Drive API v2 by default.
138   if (!command_line->HasSwitch(switches::kEnableDriveV2Api))
139     return true;
140 
141   std::string value =
142       command_line->GetSwitchValueASCII(switches::kEnableDriveV2Api);
143   StringToLowerASCII(&value);
144   // The value must be "" or "true" for true, or "false" for false.
145   DCHECK(value.empty() || value == "true" || value == "false");
146   return value != "false";
147 }
148 
EscapeQueryStringValue(const std::string & str)149 std::string EscapeQueryStringValue(const std::string& str) {
150   std::string result;
151   result.reserve(str.size());
152   for (size_t i = 0; i < str.size(); ++i) {
153     if (str[i] == '\\' || str[i] == '\'') {
154       result.push_back('\\');
155     }
156     result.push_back(str[i]);
157   }
158   return result;
159 }
160 
TranslateQuery(const std::string & original_query)161 std::string TranslateQuery(const std::string& original_query) {
162   // In order to handle non-ascii white spaces correctly, convert to UTF16.
163   base::string16 query = UTF8ToUTF16(original_query);
164   const base::string16 kDelimiter(
165       base::kWhitespaceUTF16 + base::string16(1, static_cast<char16>('"')));
166 
167   std::string result;
168   for (size_t index = query.find_first_not_of(base::kWhitespaceUTF16);
169        index != base::string16::npos;
170        index = query.find_first_not_of(base::kWhitespaceUTF16, index)) {
171     bool is_exclusion = (query[index] == '-');
172     if (is_exclusion)
173       ++index;
174     if (index == query.length()) {
175       // Here, the token is '-' and it should be ignored.
176       continue;
177     }
178 
179     size_t begin_token = index;
180     base::string16 token;
181     if (query[begin_token] == '"') {
182       // Quoted query.
183       ++begin_token;
184       size_t end_token = query.find('"', begin_token);
185       if (end_token == base::string16::npos) {
186         // This is kind of syntax error, since quoted string isn't finished.
187         // However, the query is built by user manually, so here we treat
188         // whole remaining string as a token as a fallback, by appending
189         // a missing double-quote character.
190         end_token = query.length();
191         query.push_back('"');
192       }
193 
194       token = query.substr(begin_token, end_token - begin_token);
195       index = end_token + 1;  // Consume last '"', too.
196     } else {
197       size_t end_token = query.find_first_of(kDelimiter, begin_token);
198       if (end_token == base::string16::npos) {
199         end_token = query.length();
200       }
201 
202       token = query.substr(begin_token, end_token - begin_token);
203       index = end_token;
204     }
205 
206     if (token.empty()) {
207       // Just ignore an empty token.
208       continue;
209     }
210 
211     if (!result.empty()) {
212       // If there are two or more tokens, need to connect with "and".
213       result.append(" and ");
214     }
215 
216     // The meaning of "fullText" should include title, description and content.
217     base::StringAppendF(
218         &result,
219         "%sfullText contains \'%s\'",
220         is_exclusion ? "not " : "",
221         EscapeQueryStringValue(UTF16ToUTF8(token)).c_str());
222   }
223 
224   return result;
225 }
226 
ExtractResourceIdFromUrl(const GURL & url)227 std::string ExtractResourceIdFromUrl(const GURL& url) {
228   return net::UnescapeURLComponent(url.ExtractFileName(),
229                                    net::UnescapeRule::URL_SPECIAL_CHARS);
230 }
231 
CanonicalizeResourceId(const std::string & resource_id)232 std::string CanonicalizeResourceId(const std::string& resource_id) {
233   // If resource ID is in the old WAPI format starting with a prefix like
234   // "document:", strip it and return the remaining part.
235   std::string stripped_resource_id;
236   if (RE2::FullMatch(resource_id, "^[a-z-]+(?::|%3A)([\\w-]+)$",
237                      &stripped_resource_id))
238     return stripped_resource_id;
239   return resource_id;
240 }
241 
GetIdentityResourceIdCanonicalizer()242 ResourceIdCanonicalizer GetIdentityResourceIdCanonicalizer() {
243   return base::Bind(&Identity);
244 }
245 
246 const char kDocsListScope[] = "https://docs.google.com/feeds/";
247 const char kDriveAppsScope[] = "https://www.googleapis.com/auth/drive.apps";
248 
ParseShareUrlAndRun(const google_apis::GetShareUrlCallback & callback,google_apis::GDataErrorCode error,scoped_ptr<base::Value> value)249 void ParseShareUrlAndRun(const google_apis::GetShareUrlCallback& callback,
250                          google_apis::GDataErrorCode error,
251                          scoped_ptr<base::Value> value) {
252   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
253 
254   if (!value) {
255     callback.Run(error, GURL());
256     return;
257   }
258 
259   // Parsing ResourceEntry is cheap enough to do on UI thread.
260   scoped_ptr<google_apis::ResourceEntry> entry =
261       google_apis::ResourceEntry::ExtractAndParse(*value);
262   if (!entry) {
263     callback.Run(google_apis::GDATA_PARSE_ERROR, GURL());
264     return;
265   }
266 
267   const google_apis::Link* share_link =
268       entry->GetLinkByType(google_apis::Link::LINK_SHARE);
269   callback.Run(error, share_link ? share_link->href() : GURL());
270 }
271 
272 scoped_ptr<google_apis::AboutResource>
ConvertAccountMetadataToAboutResource(const google_apis::AccountMetadata & account_metadata,const std::string & root_resource_id)273 ConvertAccountMetadataToAboutResource(
274     const google_apis::AccountMetadata& account_metadata,
275     const std::string& root_resource_id) {
276   scoped_ptr<google_apis::AboutResource> resource(
277       new google_apis::AboutResource);
278   resource->set_largest_change_id(account_metadata.largest_changestamp());
279   resource->set_quota_bytes_total(account_metadata.quota_bytes_total());
280   resource->set_quota_bytes_used(account_metadata.quota_bytes_used());
281   resource->set_root_folder_id(root_resource_id);
282   return resource.Pass();
283 }
284 
285 scoped_ptr<google_apis::AppList>
ConvertAccountMetadataToAppList(const google_apis::AccountMetadata & account_metadata)286 ConvertAccountMetadataToAppList(
287     const google_apis::AccountMetadata& account_metadata) {
288   scoped_ptr<google_apis::AppList> resource(new google_apis::AppList);
289 
290   const ScopedVector<google_apis::InstalledApp>& installed_apps =
291       account_metadata.installed_apps();
292   ScopedVector<google_apis::AppResource> app_resources;
293   app_resources.reserve(installed_apps.size());
294   for (size_t i = 0; i < installed_apps.size(); ++i) {
295     app_resources.push_back(
296         ConvertInstalledAppToAppResource(*installed_apps[i]).release());
297   }
298   resource->set_items(app_resources.Pass());
299 
300   // etag is not supported in AccountMetadata.
301 
302   return resource.Pass();
303 }
304 
305 
ConvertResourceEntryToFileResource(const google_apis::ResourceEntry & entry)306 scoped_ptr<google_apis::FileResource> ConvertResourceEntryToFileResource(
307     const google_apis::ResourceEntry& entry) {
308   scoped_ptr<google_apis::FileResource> file(new google_apis::FileResource);
309 
310   file->set_file_id(entry.resource_id());
311   file->set_title(entry.title());
312   file->set_created_date(entry.published_time());
313 
314   if (std::find(entry.labels().begin(), entry.labels().end(),
315                 "shared-with-me") != entry.labels().end()) {
316     // Set current time to mark the file is shared_with_me, since ResourceEntry
317     // doesn't have |shared_with_me_date| equivalent.
318     file->set_shared_with_me_date(base::Time::Now());
319   }
320 
321   file->set_shared(std::find(entry.labels().begin(), entry.labels().end(),
322                              "shared") != entry.labels().end());
323 
324   file->set_download_url(entry.download_url());
325   if (entry.is_folder())
326     file->set_mime_type(kDriveFolderMimeType);
327   else
328     file->set_mime_type(entry.content_mime_type());
329 
330   file->set_md5_checksum(entry.file_md5());
331   file->set_file_size(entry.file_size());
332 
333   file->mutable_labels()->set_trashed(entry.deleted());
334   file->set_etag(entry.etag());
335 
336   google_apis::ImageMediaMetadata* image_media_metadata =
337     file->mutable_image_media_metadata();
338   image_media_metadata->set_width(entry.image_width());
339   image_media_metadata->set_height(entry.image_height());
340   image_media_metadata->set_rotation(entry.image_rotation());
341 
342   ScopedVector<google_apis::ParentReference> parents;
343   for (size_t i = 0; i < entry.links().size(); ++i) {
344     using google_apis::Link;
345     const Link& link = *entry.links()[i];
346     switch (link.type()) {
347       case Link::LINK_PARENT: {
348         scoped_ptr<google_apis::ParentReference> parent(
349             new google_apis::ParentReference);
350         parent->set_parent_link(link.href());
351 
352         std::string file_id =
353             drive::util::ExtractResourceIdFromUrl(link.href());
354         parent->set_file_id(file_id);
355         parent->set_is_root(file_id == kWapiRootDirectoryResourceId);
356         parents.push_back(parent.release());
357         break;
358       }
359       case Link::LINK_EDIT:
360         file->set_self_link(link.href());
361         break;
362       case Link::LINK_THUMBNAIL:
363         file->set_thumbnail_link(link.href());
364         break;
365       case Link::LINK_ALTERNATE:
366         file->set_alternate_link(link.href());
367         break;
368       case Link::LINK_EMBED:
369         file->set_embed_link(link.href());
370         break;
371       default:
372         break;
373     }
374   }
375   file->set_parents(parents.Pass());
376 
377   file->set_modified_date(entry.updated_time());
378   file->set_last_viewed_by_me_date(entry.last_viewed_time());
379 
380   return file.Pass();
381 }
382 
GetKind(const google_apis::FileResource & file_resource)383 google_apis::DriveEntryKind GetKind(
384     const google_apis::FileResource& file_resource) {
385   if (file_resource.IsDirectory())
386     return google_apis::ENTRY_KIND_FOLDER;
387 
388   const std::string& mime_type = file_resource.mime_type();
389   if (mime_type == kGoogleDocumentMimeType)
390     return google_apis::ENTRY_KIND_DOCUMENT;
391   if (mime_type == kGoogleSpreadsheetMimeType)
392     return google_apis::ENTRY_KIND_SPREADSHEET;
393   if (mime_type == kGooglePresentationMimeType)
394     return google_apis::ENTRY_KIND_PRESENTATION;
395   if (mime_type == kGoogleDrawingMimeType)
396     return google_apis::ENTRY_KIND_DRAWING;
397   if (mime_type == kGoogleTableMimeType)
398     return google_apis::ENTRY_KIND_TABLE;
399   if (mime_type == kGoogleFormMimeType)
400     return google_apis::ENTRY_KIND_FORM;
401   if (mime_type == "application/pdf")
402     return google_apis::ENTRY_KIND_PDF;
403   return google_apis::ENTRY_KIND_FILE;
404 }
405 
406 scoped_ptr<google_apis::ResourceEntry>
ConvertFileResourceToResourceEntry(const google_apis::FileResource & file_resource)407 ConvertFileResourceToResourceEntry(
408     const google_apis::FileResource& file_resource) {
409   scoped_ptr<google_apis::ResourceEntry> entry(new google_apis::ResourceEntry);
410 
411   // ResourceEntry
412   entry->set_resource_id(file_resource.file_id());
413   entry->set_id(file_resource.file_id());
414   entry->set_kind(GetKind(file_resource));
415   entry->set_title(file_resource.title());
416   entry->set_published_time(file_resource.created_date());
417 
418   std::vector<std::string> labels;
419   if (!file_resource.shared_with_me_date().is_null())
420     labels.push_back("shared-with-me");
421   if (file_resource.shared())
422     labels.push_back("shared");
423   entry->set_labels(labels);
424 
425   // This should be the url to download the file_resource.
426   {
427     google_apis::Content content;
428     content.set_url(file_resource.download_url());
429     content.set_mime_type(file_resource.mime_type());
430     entry->set_content(content);
431   }
432   // TODO(kochi): entry->resource_links_
433 
434   // For file entries
435   entry->set_filename(file_resource.title());
436   entry->set_suggested_filename(file_resource.title());
437   entry->set_file_md5(file_resource.md5_checksum());
438   entry->set_file_size(file_resource.file_size());
439 
440   // If file is removed completely, that information is only available in
441   // ChangeResource, and is reflected in |removed_|. If file is trashed, the
442   // file entry still exists but with its "trashed" label true.
443   entry->set_deleted(file_resource.labels().is_trashed());
444 
445   // ImageMediaMetadata
446   entry->set_image_width(file_resource.image_media_metadata().width());
447   entry->set_image_height(file_resource.image_media_metadata().height());
448   entry->set_image_rotation(file_resource.image_media_metadata().rotation());
449 
450   // CommonMetadata
451   entry->set_etag(file_resource.etag());
452   // entry->authors_
453   // entry->links_.
454   ScopedVector<google_apis::Link> links;
455   for (size_t i = 0; i < file_resource.parents().size(); ++i) {
456     google_apis::Link* link = new google_apis::Link;
457     link->set_type(google_apis::Link::LINK_PARENT);
458     link->set_href(file_resource.parents()[i]->parent_link());
459     links.push_back(link);
460   }
461   if (!file_resource.self_link().is_empty()) {
462     google_apis::Link* link = new google_apis::Link;
463     link->set_type(google_apis::Link::LINK_EDIT);
464     link->set_href(file_resource.self_link());
465     links.push_back(link);
466   }
467   if (!file_resource.thumbnail_link().is_empty()) {
468     google_apis::Link* link = new google_apis::Link;
469     link->set_type(google_apis::Link::LINK_THUMBNAIL);
470     link->set_href(file_resource.thumbnail_link());
471     links.push_back(link);
472   }
473   if (!file_resource.alternate_link().is_empty()) {
474     google_apis::Link* link = new google_apis::Link;
475     link->set_type(google_apis::Link::LINK_ALTERNATE);
476     link->set_href(file_resource.alternate_link());
477     links.push_back(link);
478   }
479   if (!file_resource.embed_link().is_empty()) {
480     google_apis::Link* link = new google_apis::Link;
481     link->set_type(google_apis::Link::LINK_EMBED);
482     link->set_href(file_resource.embed_link());
483     links.push_back(link);
484   }
485   entry->set_links(links.Pass());
486 
487   // entry->categories_
488   entry->set_updated_time(file_resource.modified_date());
489   entry->set_last_viewed_time(file_resource.last_viewed_by_me_date());
490 
491   entry->FillRemainingFields();
492   return entry.Pass();
493 }
494 
495 scoped_ptr<google_apis::ResourceEntry>
ConvertChangeResourceToResourceEntry(const google_apis::ChangeResource & change_resource)496 ConvertChangeResourceToResourceEntry(
497     const google_apis::ChangeResource& change_resource) {
498   scoped_ptr<google_apis::ResourceEntry> entry;
499   if (change_resource.file())
500     entry = ConvertFileResourceToResourceEntry(*change_resource.file()).Pass();
501   else
502     entry.reset(new google_apis::ResourceEntry);
503 
504   entry->set_resource_id(change_resource.file_id());
505   // If |is_deleted()| returns true, the file is removed from Drive.
506   entry->set_removed(change_resource.is_deleted());
507   entry->set_changestamp(change_resource.change_id());
508 
509   return entry.Pass();
510 }
511 
512 scoped_ptr<google_apis::ResourceList>
ConvertFileListToResourceList(const google_apis::FileList & file_list)513 ConvertFileListToResourceList(const google_apis::FileList& file_list) {
514   scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
515 
516   const ScopedVector<google_apis::FileResource>& items = file_list.items();
517   ScopedVector<google_apis::ResourceEntry> entries;
518   for (size_t i = 0; i < items.size(); ++i)
519     entries.push_back(ConvertFileResourceToResourceEntry(*items[i]).release());
520   feed->set_entries(entries.Pass());
521 
522   ScopedVector<google_apis::Link> links;
523   if (!file_list.next_link().is_empty()) {
524     google_apis::Link* link = new google_apis::Link;
525     link->set_type(google_apis::Link::LINK_NEXT);
526     link->set_href(file_list.next_link());
527     links.push_back(link);
528   }
529   feed->set_links(links.Pass());
530 
531   return feed.Pass();
532 }
533 
534 scoped_ptr<google_apis::ResourceList>
ConvertChangeListToResourceList(const google_apis::ChangeList & change_list)535 ConvertChangeListToResourceList(const google_apis::ChangeList& change_list) {
536   scoped_ptr<google_apis::ResourceList> feed(new google_apis::ResourceList);
537 
538   const ScopedVector<google_apis::ChangeResource>& items = change_list.items();
539   ScopedVector<google_apis::ResourceEntry> entries;
540   for (size_t i = 0; i < items.size(); ++i) {
541     entries.push_back(
542         ConvertChangeResourceToResourceEntry(*items[i]).release());
543   }
544   feed->set_entries(entries.Pass());
545 
546   feed->set_largest_changestamp(change_list.largest_change_id());
547 
548   ScopedVector<google_apis::Link> links;
549   if (!change_list.next_link().is_empty()) {
550     google_apis::Link* link = new google_apis::Link;
551     link->set_type(google_apis::Link::LINK_NEXT);
552     link->set_href(change_list.next_link());
553     links.push_back(link);
554   }
555   feed->set_links(links.Pass());
556 
557   return feed.Pass();
558 }
559 
GetMd5Digest(const base::FilePath & file_path)560 std::string GetMd5Digest(const base::FilePath& file_path) {
561   const int kBufferSize = 512 * 1024;  // 512kB.
562 
563   base::PlatformFile file = base::CreatePlatformFile(
564       file_path, base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_READ,
565       NULL, NULL);
566   if (file == base::kInvalidPlatformFileValue)
567     return std::string();
568   base::ScopedPlatformFileCloser file_closer(&file);
569 
570   base::MD5Context context;
571   base::MD5Init(&context);
572 
573   int64 offset = 0;
574   scoped_ptr<char[]> buffer(new char[kBufferSize]);
575   while (true) {
576     // Avoid using ReadPlatformFileCurPosNoBestEffort for now.
577     // http://crbug.com/145873
578     int result = base::ReadPlatformFileNoBestEffort(
579         file, offset, buffer.get(), kBufferSize);
580 
581     if (result < 0) {
582       // Found an error.
583       return std::string();
584     }
585 
586     if (result == 0) {
587       // End of file.
588       break;
589     }
590 
591     offset += result;
592     base::MD5Update(&context, base::StringPiece(buffer.get(), result));
593   }
594 
595   base::MD5Digest digest;
596   base::MD5Final(&digest, &context);
597   return MD5DigestToBase16(digest);
598 }
599 
600 const char kWapiRootDirectoryResourceId[] = "folder:root";
601 
602 }  // namespace util
603 }  // namespace drive
604