1// Copyright 2013 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/utility/media_galleries/iphoto_library_parser.h"
6
7#include <string>
8
9#include "base/logging.h"
10#include "base/stl_util.h"
11#include "base/strings/string_number_conversions.h"
12#include "base/strings/string_util.h"
13#include "chrome/utility/media_galleries/iapps_xml_utils.h"
14#include "third_party/libxml/chromium/libxml_utils.h"
15
16namespace iphoto {
17
18namespace {
19
20struct PhotoInfo {
21  uint64 id;
22  base::FilePath location;
23  base::FilePath original_location;
24};
25
26struct AlbumInfo {
27  std::set<uint64> photo_ids;
28  std::string name;
29  uint64 id;
30};
31
32class PhotosXmlDictReader : public iapps::XmlDictReader {
33 public:
34  PhotosXmlDictReader(XmlReader* reader, PhotoInfo* photo_info)
35    : iapps::XmlDictReader(reader), photo_info_(photo_info) {}
36
37  virtual bool HandleKeyImpl(const std::string& key) OVERRIDE {
38    if (key == "ImagePath") {
39      std::string value;
40      if (!iapps::ReadString(reader_, &value))
41        return false;
42      photo_info_->location = base::FilePath(value);
43    } else if (key == "OriginalPath") {
44      std::string value;
45      if (!iapps::ReadString(reader_, &value))
46        return false;
47      photo_info_->original_location = base::FilePath(value);
48    } else if (!SkipToNext()) {
49      return false;
50    }
51    return true;
52  }
53
54  virtual bool FinishedOk() OVERRIDE {
55    return Found("ImagePath");
56  }
57
58 private:
59  PhotoInfo* photo_info_;
60};
61
62// Contents of the album 'KeyList' key are
63// <array>
64//  <string>1</string>
65//  <string>2</string>
66//  <string>3</string>
67// </array>
68bool ReadStringArray(XmlReader* reader, std::set<uint64>* photo_ids) {
69  if (reader->NodeName() != "array")
70    return false;
71
72  // Advance past the array node and into the body of the array.
73  if (!reader->Read())
74    return false;
75
76  int array_content_depth = reader->Depth();
77
78  while (iapps::SeekToNodeAtCurrentDepth(reader, "string")) {
79    if (reader->Depth() != array_content_depth)
80      return false;
81    std::string photo_id;
82    if (!iapps::ReadString(reader, &photo_id))
83      continue;
84    uint64 id;
85    if (!base::StringToUint64(photo_id, &id))
86      continue;
87    photo_ids->insert(id);
88  }
89
90  return true;
91}
92
93class AlbumXmlDictReader : public iapps::XmlDictReader {
94 public:
95  AlbumXmlDictReader(XmlReader* reader, AlbumInfo* album_info)
96    : iapps::XmlDictReader(reader), album_info_(album_info) {}
97
98  virtual bool ShouldLoop() OVERRIDE {
99    return !(Found("AlbumId") && Found("AlbumName") && Found("KeyList"));
100  }
101
102  virtual bool HandleKeyImpl(const std::string& key) OVERRIDE {
103    if (key == "AlbumId") {
104      if (!iapps::ReadInteger(reader_, &album_info_->id))
105        return false;
106    } else if (key == "AlbumName") {
107      if (!iapps::ReadString(reader_, &album_info_->name))
108        return false;
109    } else if (key == "KeyList") {
110      if (!iapps::SeekToNodeAtCurrentDepth(reader_, "array"))
111        return false;
112      if (!ReadStringArray(reader_, &album_info_->photo_ids))
113        return false;
114    } else if (!SkipToNext()) {
115      return false;
116    }
117    return true;
118  }
119
120  virtual bool FinishedOk() OVERRIDE {
121    return !ShouldLoop();
122  }
123
124 private:
125  AlbumInfo* album_info_;
126};
127
128// Inside the master image list, we expect photos to be arranged as
129//  <dict>
130//   <key>$PHOTO_ID</key>
131//   <dict>
132//     $photo properties
133//   </dict>
134//   <key>$PHOTO_ID</key>
135//   <dict>
136//     $photo properties
137//   </dict>
138//   ...
139//  </dict>
140// Returns true on success, false on error.
141bool ParseAllPhotos(XmlReader* reader,
142                    std::set<iphoto::parser::Photo>* all_photos) {
143  if (!iapps::SeekToNodeAtCurrentDepth(reader, "dict"))
144    return false;
145  int photos_dict_depth = reader->Depth() + 1;
146  if (!reader->Read())
147    return false;
148
149  bool errors = false;
150  while (reader->Depth() >= photos_dict_depth) {
151    if (!iapps::SeekToNodeAtCurrentDepth(reader, "key"))
152      break;
153
154    std::string key;
155    if (!reader->ReadElementContent(&key)) {
156      errors = true;
157      break;
158    }
159    uint64 id;
160    bool id_valid = base::StringToUint64(key, &id);
161
162    if (!id_valid ||
163        reader->Depth() != photos_dict_depth) {
164      errors = true;
165      break;
166    }
167    if (!iapps::SeekToNodeAtCurrentDepth(reader, "dict")) {
168      errors = true;
169      break;
170    }
171
172    PhotoInfo photo_info;
173    photo_info.id = id;
174    // Walk through a dictionary filling in |result| with photo information.
175    // Return true if at least the location was found.
176    // In either case, the cursor is advanced out of the dictionary.
177    PhotosXmlDictReader dict_reader(reader, &photo_info);
178    if (!dict_reader.Read()) {
179      errors = true;
180      break;
181    }
182
183    parser::Photo photo(photo_info.id, photo_info.location,
184                        photo_info.original_location);
185    all_photos->insert(photo);
186  }
187
188  return !errors;
189}
190
191}  // namespace
192
193IPhotoLibraryParser::IPhotoLibraryParser() {}
194IPhotoLibraryParser::~IPhotoLibraryParser() {}
195
196class IPhotoLibraryXmlDictReader : public iapps::XmlDictReader {
197 public:
198  IPhotoLibraryXmlDictReader(XmlReader* reader, parser::Library* library)
199    : iapps::XmlDictReader(reader), library_(library), ok_(true) {}
200
201  virtual bool ShouldLoop() OVERRIDE {
202    return !(Found("List of Albums") && Found("Master Image List"));
203  }
204
205  virtual bool HandleKeyImpl(const std::string& key) OVERRIDE {
206    if (key == "List of Albums") {
207      if (!iapps::SeekToNodeAtCurrentDepth(reader_, "array") ||
208          !reader_->Read()) {
209        return true;
210      }
211      while (iapps::SeekToNodeAtCurrentDepth(reader_, "dict")) {
212        AlbumInfo album_info;
213        AlbumXmlDictReader dict_reader(reader_, &album_info);
214        if (dict_reader.Read()) {
215          parser::Album album;
216          album = album_info.photo_ids;
217          // Strip / from album name and dedupe any collisions.
218          std::string name;
219          base::ReplaceChars(album_info.name, "//", " ", &name);
220          if (ContainsKey(library_->albums, name))
221            name = name + "("+base::Uint64ToString(album_info.id)+")";
222          library_->albums[name] = album;
223        }
224      }
225    } else if (key == "Master Image List") {
226      if (!ParseAllPhotos(reader_, &library_->all_photos)) {
227        ok_ = false;
228        return false;
229      }
230    }
231    return true;
232  }
233
234  virtual bool FinishedOk() OVERRIDE {
235    return ok_;
236  }
237
238  // The IPhotoLibrary allows duplicate "List of Albums" and
239  // "Master Image List" keys (although that seems odd.)
240  virtual bool AllowRepeats() OVERRIDE {
241    return true;
242  }
243
244 private:
245  parser::Library* library_;
246
247  // The base class bails when we request, and then calls |FinishedOk()|
248  // to decide what to return. We need to remember that we bailed because
249  // of an error. That's what |ok_| does.
250  bool ok_;
251};
252
253bool IPhotoLibraryParser::Parse(const std::string& library_xml) {
254  XmlReader reader;
255  if (!reader.Load(library_xml))
256    return false;
257
258  // Find the plist node and then search within that tag.
259  if (!iapps::SeekToNodeAtCurrentDepth(&reader, "plist"))
260    return false;
261  if (!reader.Read())
262    return false;
263
264  if (!iapps::SeekToNodeAtCurrentDepth(&reader, "dict"))
265    return false;
266
267  IPhotoLibraryXmlDictReader dict_reader(&reader, &library_);
268  return dict_reader.Read();
269}
270
271}  // namespace iphoto
272