• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python2.4
2# Copyright (c) 2010 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""An implementation of the server side of the Chromium sync protocol.
7
8The details of the protocol are described mostly by comments in the protocol
9buffer definition at chrome/browser/sync/protocol/sync.proto.
10"""
11
12import cgi
13import copy
14import operator
15import random
16import sys
17import threading
18
19import app_specifics_pb2
20import autofill_specifics_pb2
21import bookmark_specifics_pb2
22import extension_specifics_pb2
23import nigori_specifics_pb2
24import password_specifics_pb2
25import preference_specifics_pb2
26import session_specifics_pb2
27import sync_pb2
28import theme_specifics_pb2
29import typed_url_specifics_pb2
30
31# An enumeration of the various kinds of data that can be synced.
32# Over the wire, this enumeration is not used: a sync object's type is
33# inferred by which EntitySpecifics extension it has.  But in the context
34# of a program, it is useful to have an enumeration.
35ALL_TYPES = (
36    TOP_LEVEL,  # The type of the 'Google Chrome' folder.
37    APPS,
38    AUTOFILL,
39    AUTOFILL_PROFILE,
40    BOOKMARK,
41    EXTENSIONS,
42    NIGORI,
43    PASSWORD,
44    PREFERENCE,
45    SESSION,
46    THEME,
47    TYPED_URL) = range(12)
48
49# Well-known server tag of the top level "Google Chrome" folder.
50TOP_LEVEL_FOLDER_TAG = 'google_chrome'
51
52# Given a sync type from ALL_TYPES, find the extension token corresponding
53# to that datatype.  Note that TOP_LEVEL has no such token.
54SYNC_TYPE_TO_EXTENSION = {
55    APPS: app_specifics_pb2.app,
56    AUTOFILL: autofill_specifics_pb2.autofill,
57    AUTOFILL_PROFILE: autofill_specifics_pb2.autofill_profile,
58    BOOKMARK: bookmark_specifics_pb2.bookmark,
59    EXTENSIONS: extension_specifics_pb2.extension,
60    NIGORI: nigori_specifics_pb2.nigori,
61    PASSWORD: password_specifics_pb2.password,
62    PREFERENCE: preference_specifics_pb2.preference,
63    SESSION: session_specifics_pb2.session,
64    THEME: theme_specifics_pb2.theme,
65    TYPED_URL: typed_url_specifics_pb2.typed_url,
66    }
67
68# The parent ID used to indicate a top-level node.
69ROOT_ID = '0'
70
71
72class Error(Exception):
73  """Error class for this module."""
74
75
76class ProtobufExtensionNotUnique(Error):
77  """An entry should not have more than one protobuf extension present."""
78
79
80class DataTypeIdNotRecognized(Error):
81  """The requested data type is not recognized."""
82
83
84def GetEntryType(entry):
85  """Extract the sync type from a SyncEntry.
86
87  Args:
88    entry: A SyncEntity protobuf object whose type to determine.
89  Returns:
90    A value from ALL_TYPES if the entry's type can be determined, or None
91    if the type cannot be determined.
92  Raises:
93    ProtobufExtensionNotUnique: More than one type was indicated by the entry.
94  """
95  if entry.server_defined_unique_tag == TOP_LEVEL_FOLDER_TAG:
96    return TOP_LEVEL
97  entry_types = GetEntryTypesFromSpecifics(entry.specifics)
98  if not entry_types:
99    return None
100
101  # If there is more than one, either there's a bug, or else the caller
102  # should use GetEntryTypes.
103  if len(entry_types) > 1:
104    raise ProtobufExtensionNotUnique
105  return entry_types[0]
106
107
108def GetEntryTypesFromSpecifics(specifics):
109  """Determine the sync types indicated by an EntitySpecifics's extension(s).
110
111  If the specifics have more than one recognized extension (as commonly
112  happens with the requested_types field of GetUpdatesMessage), all types
113  will be returned.  Callers must handle the possibility of the returned
114  value having more than one item.
115
116  Args:
117    specifics: A EntitySpecifics protobuf message whose extensions to
118      enumerate.
119  Returns:
120    A list of the sync types (values from ALL_TYPES) assocated with each
121    recognized extension of the specifics message.
122  """
123  return [data_type for data_type, extension
124          in SYNC_TYPE_TO_EXTENSION.iteritems()
125          if specifics.HasExtension(extension)]
126
127
128def SyncTypeToProtocolDataTypeId(data_type):
129  """Convert from a sync type (python enum) to the protocol's data type id."""
130  return SYNC_TYPE_TO_EXTENSION[data_type].number
131
132
133def ProtocolDataTypeIdToSyncType(protocol_data_type_id):
134  """Convert from the protocol's data type id to a sync type (python enum)."""
135  for data_type, protocol_extension in SYNC_TYPE_TO_EXTENSION.iteritems():
136    if protocol_extension.number == protocol_data_type_id:
137      return data_type
138  raise DataTypeIdNotRecognized
139
140
141def GetDefaultEntitySpecifics(data_type):
142  """Get an EntitySpecifics having a sync type's default extension value."""
143  specifics = sync_pb2.EntitySpecifics()
144  if data_type in SYNC_TYPE_TO_EXTENSION:
145    extension_handle = SYNC_TYPE_TO_EXTENSION[data_type]
146    specifics.Extensions[extension_handle].SetInParent()
147  return specifics
148
149
150def DeepCopyOfProto(proto):
151  """Return a deep copy of a protocol buffer."""
152  new_proto = type(proto)()
153  new_proto.MergeFrom(proto)
154  return new_proto
155
156
157class PermanentItem(object):
158  """A specification of one server-created permanent item.
159
160  Attributes:
161    tag: A known-to-the-client value that uniquely identifies a server-created
162      permanent item.
163    name: The human-readable display name for this item.
164    parent_tag: The tag of the permanent item's parent.  If ROOT_ID, indicates
165      a top-level item.  Otherwise, this must be the tag value of some other
166      server-created permanent item.
167    sync_type: A value from ALL_TYPES, giving the datatype of this permanent
168      item.  This controls which types of client GetUpdates requests will
169      cause the permanent item to be created and returned.
170  """
171
172  def __init__(self, tag, name, parent_tag, sync_type):
173    self.tag = tag
174    self.name = name
175    self.parent_tag = parent_tag
176    self.sync_type = sync_type
177
178
179class UpdateSieve(object):
180  """A filter to remove items the client has already seen."""
181  def __init__(self, request):
182    self._original_request = request
183    self._state = {}
184    if request.from_progress_marker:
185      for marker in request.from_progress_marker:
186        if marker.HasField("timestamp_token_for_migration"):
187          timestamp = marker.timestamp_token_for_migration
188        elif marker.token:
189          timestamp = int(marker.token)
190        elif marker.HasField("token"):
191          timestamp = 0
192        else:
193          raise ValueError("No timestamp information in progress marker.")
194        data_type = ProtocolDataTypeIdToSyncType(marker.data_type_id)
195        self._state[data_type] = timestamp
196    elif request.HasField("from_timestamp"):
197      for data_type in GetEntryTypesFromSpecifics(request.requested_types):
198        self._state[data_type] = request.from_timestamp
199    if self._state:
200      self._state[TOP_LEVEL] = min(self._state.itervalues())
201
202  def ClientWantsItem(self, item):
203    """Return true if the client hasn't already seen an item."""
204    return self._state.get(GetEntryType(item), sys.maxint) < item.version
205
206  def HasAnyTimestamp(self):
207    """Return true if at least one datatype was requested."""
208    return bool(self._state)
209
210  def GetMinTimestamp(self):
211    """Return true the smallest timestamp requested across all datatypes."""
212    return min(self._state.itervalues())
213
214  def GetFirstTimeTypes(self):
215    """Return a list of datatypes requesting updates from timestamp zero."""
216    return [datatype for datatype, timestamp in self._state.iteritems()
217            if timestamp == 0]
218
219  def SaveProgress(self, new_timestamp, get_updates_response):
220    """Write the new_timestamp or new_progress_marker fields to a response."""
221    if self._original_request.from_progress_marker:
222      for data_type, old_timestamp in self._state.iteritems():
223        if data_type == TOP_LEVEL:
224          continue
225        new_marker = sync_pb2.DataTypeProgressMarker()
226        new_marker.data_type_id = SyncTypeToProtocolDataTypeId(data_type)
227        new_marker.token = str(max(old_timestamp, new_timestamp))
228        if new_marker not in self._original_request.from_progress_marker:
229          get_updates_response.new_progress_marker.add().MergeFrom(new_marker)
230    elif self._original_request.HasField("from_timestamp"):
231      if self._original_request.from_timestamp < new_timestamp:
232        get_updates_response.new_timestamp = new_timestamp
233
234
235class SyncDataModel(object):
236  """Models the account state of one sync user."""
237  _BATCH_SIZE = 100
238
239  # Specify all the permanent items that a model might need.
240  _PERMANENT_ITEM_SPECS = [
241      PermanentItem('google_chrome', name='Google Chrome',
242                    parent_tag=ROOT_ID, sync_type=TOP_LEVEL),
243      PermanentItem('google_chrome_bookmarks', name='Bookmarks',
244                    parent_tag='google_chrome', sync_type=BOOKMARK),
245      PermanentItem('bookmark_bar', name='Bookmark Bar',
246                    parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK),
247      PermanentItem('other_bookmarks', name='Other Bookmarks',
248                    parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK),
249      PermanentItem('google_chrome_preferences', name='Preferences',
250                    parent_tag='google_chrome', sync_type=PREFERENCE),
251      PermanentItem('google_chrome_autofill', name='Autofill',
252                    parent_tag='google_chrome', sync_type=AUTOFILL),
253      PermanentItem('google_chrome_autofill_profiles', name='Autofill Profiles',
254                    parent_tag='google_chrome', sync_type=AUTOFILL_PROFILE),
255      PermanentItem('google_chrome_extensions', name='Extensions',
256                    parent_tag='google_chrome', sync_type=EXTENSIONS),
257      PermanentItem('google_chrome_passwords', name='Passwords',
258                    parent_tag='google_chrome', sync_type=PASSWORD),
259      PermanentItem('google_chrome_sessions', name='Sessions',
260                    parent_tag='google_chrome', sync_type=SESSION),
261      PermanentItem('google_chrome_themes', name='Themes',
262                    parent_tag='google_chrome', sync_type=THEME),
263      PermanentItem('google_chrome_typed_urls', name='Typed URLs',
264                    parent_tag='google_chrome', sync_type=TYPED_URL),
265      PermanentItem('google_chrome_nigori', name='Nigori',
266                    parent_tag='google_chrome', sync_type=NIGORI),
267      PermanentItem('google_chrome_apps', name='Apps',
268                    parent_tag='google_chrome', sync_type=APPS),
269      ]
270
271  def __init__(self):
272    # Monotonically increasing version number.  The next object change will
273    # take on this value + 1.
274    self._version = 0
275
276    # The definitive copy of this client's items: a map from ID string to a
277    # SyncEntity protocol buffer.
278    self._entries = {}
279
280    # TODO(nick): uuid.uuid1() is better, but python 2.5 only.
281    self.store_birthday = '%0.30f' % random.random()
282
283  def _SaveEntry(self, entry):
284    """Insert or update an entry in the change log, and give it a new version.
285
286    The ID fields of this entry are assumed to be valid server IDs.  This
287    entry will be updated with a new version number and sync_timestamp.
288
289    Args:
290      entry: The entry to be added or updated.
291    """
292    self._version += 1
293    # Maintain a global (rather than per-item) sequence number and use it
294    # both as the per-entry version as well as the update-progress timestamp.
295    # This simulates the behavior of the original server implementation.
296    entry.version = self._version
297    entry.sync_timestamp = self._version
298
299    # Preserve the originator info, which the client is not required to send
300    # when updating.
301    base_entry = self._entries.get(entry.id_string)
302    if base_entry:
303      entry.originator_cache_guid = base_entry.originator_cache_guid
304      entry.originator_client_item_id = base_entry.originator_client_item_id
305
306    self._entries[entry.id_string] = DeepCopyOfProto(entry)
307
308  def _ServerTagToId(self, tag):
309    """Determine the server ID from a server-unique tag.
310
311    The resulting value is guaranteed not to collide with the other ID
312    generation methods.
313
314    Args:
315      tag: The unique, known-to-the-client tag of a server-generated item.
316    Returns:
317      The string value of the computed server ID.
318    """
319    if tag and tag != ROOT_ID:
320      return '<server tag>%s' % tag
321    else:
322      return tag
323
324  def _ClientTagToId(self, tag):
325    """Determine the server ID from a client-unique tag.
326
327    The resulting value is guaranteed not to collide with the other ID
328    generation methods.
329
330    Args:
331      tag: The unique, opaque-to-the-server tag of a client-tagged item.
332    Returns:
333      The string value of the computed server ID.
334    """
335    return '<client tag>%s' % tag
336
337  def _ClientIdToId(self, client_guid, client_item_id):
338    """Compute a unique server ID from a client-local ID tag.
339
340    The resulting value is guaranteed not to collide with the other ID
341    generation methods.
342
343    Args:
344      client_guid: A globally unique ID that identifies the client which
345        created this item.
346      client_item_id: An ID that uniquely identifies this item on the client
347        which created it.
348    Returns:
349      The string value of the computed server ID.
350    """
351    # Using the client ID info is not required here (we could instead generate
352    # a random ID), but it's useful for debugging.
353    return '<server ID originally>%s/%s' % (client_guid, client_item_id)
354
355  def _WritePosition(self, entry, parent_id, prev_id=None):
356    """Convert from a relative position into an absolute, numeric position.
357
358    Clients specify positions using the predecessor-based references; the
359    server stores and reports item positions using sparse integer values.
360    This method converts from the former to the latter.
361
362    Args:
363      entry: The entry for which to compute a position.  Its ID field are
364        assumed to be server IDs.  This entry will have its parent_id_string
365        and position_in_parent fields updated; its insert_after_item_id field
366        will be cleared.
367      parent_id: The ID of the entry intended as the new parent.
368      prev_id: The ID of the entry intended as the new predecessor.  If this
369        is None, or an ID of an object which is not a child of the new parent,
370        the entry will be positioned at the end (right) of the ordering.  If
371        the empty ID (''), this will be positioned at the front (left) of the
372        ordering.  Otherwise, the entry will be given a position_in_parent
373        value placing it just after (to the right of) the new predecessor.
374    """
375    preferred_gap = 2 ** 20
376
377    def ExtendRange(current_limit_entry, sign_multiplier):
378      """Compute values at the beginning or end."""
379      if current_limit_entry.id_string == entry.id_string:
380        step = 0
381      else:
382        step = sign_multiplier * preferred_gap
383      return current_limit_entry.position_in_parent + step
384
385    siblings = [x for x in self._entries.values()
386                if x.parent_id_string == parent_id and not x.deleted]
387    siblings = sorted(siblings, key=operator.attrgetter('position_in_parent'))
388    if prev_id == entry.id_string:
389      prev_id = ''
390    if not siblings:
391      # First item in this container; start in the middle.
392      entry.position_in_parent = 0
393    elif not prev_id:
394      # A special value in the protocol.  Insert at first position.
395      entry.position_in_parent = ExtendRange(siblings[0], -1)
396    else:
397      # Handle mid-insertion; consider items along with their successors.
398      for item, successor in zip(siblings, siblings[1:]):
399        if item.id_string != prev_id:
400          continue
401        elif successor.id_string == entry.id_string:
402          # We're already in place; don't change anything.
403          entry.position_in_parent = successor.position_in_parent
404        else:
405          # Interpolate new position between the previous item and its
406          # existing successor.
407          entry.position_in_parent = (item.position_in_parent * 7 +
408                                      successor.position_in_parent) / 8
409        break
410      else:
411        # Insert at end. Includes the case where prev_id is None.
412        entry.position_in_parent = ExtendRange(siblings[-1], +1)
413
414    entry.parent_id_string = parent_id
415    entry.ClearField('insert_after_item_id')
416
417  def _ItemExists(self, id_string):
418    """Determine whether an item exists in the changelog."""
419    return id_string in self._entries
420
421  def _CreatePermanentItem(self, spec):
422    """Create one permanent item from its spec, if it doesn't exist.
423
424    The resulting item is added to the changelog.
425
426    Args:
427      spec: A PermanentItem object holding the properties of the item to create.
428    """
429    id_string = self._ServerTagToId(spec.tag)
430    if self._ItemExists(id_string):
431      return
432    print 'Creating permanent item: %s' % spec.name
433    entry = sync_pb2.SyncEntity()
434    entry.id_string = id_string
435    entry.non_unique_name = spec.name
436    entry.name = spec.name
437    entry.server_defined_unique_tag = spec.tag
438    entry.folder = True
439    entry.deleted = False
440    entry.specifics.CopyFrom(GetDefaultEntitySpecifics(spec.sync_type))
441    self._WritePosition(entry, self._ServerTagToId(spec.parent_tag))
442    self._SaveEntry(entry)
443
444  def _CreatePermanentItems(self, requested_types):
445    """Ensure creation of all permanent items for a given set of sync types.
446
447    Args:
448      requested_types: A list of sync data types from ALL_TYPES.
449        Permanent items of only these types will be created.
450    """
451    for spec in self._PERMANENT_ITEM_SPECS:
452      if spec.sync_type in requested_types:
453        self._CreatePermanentItem(spec)
454
455  def GetChanges(self, sieve):
456    """Get entries which have changed, oldest first.
457
458    The returned entries are limited to being _BATCH_SIZE many.  The entries
459    are returned in strict version order.
460
461    Args:
462      sieve: An update sieve to use to filter out updates the client
463        has already seen.
464    Returns:
465      A tuple of (version, entries, changes_remaining).  Version is a new
466      timestamp value, which should be used as the starting point for the
467      next query.  Entries is the batch of entries meeting the current
468      timestamp query.  Changes_remaining indicates the number of changes
469      left on the server after this batch.
470    """
471    if not sieve.HasAnyTimestamp():
472      return (0, [], 0)
473    min_timestamp = sieve.GetMinTimestamp()
474    self._CreatePermanentItems(sieve.GetFirstTimeTypes())
475    change_log = sorted(self._entries.values(),
476                        key=operator.attrgetter('version'))
477    new_changes = [x for x in change_log if x.version > min_timestamp]
478    # Pick batch_size new changes, and then filter them.  This matches
479    # the RPC behavior of the production sync server.
480    batch = new_changes[:self._BATCH_SIZE]
481    if not batch:
482      # Client is up to date.
483      return (min_timestamp, [], 0)
484
485    # Restrict batch to requested types.  Tombstones are untyped
486    # and will always get included.
487    filtered = [DeepCopyOfProto(item) for item in batch
488                if item.deleted or sieve.ClientWantsItem(item)]
489
490    # The new client timestamp is the timestamp of the last item in the
491    # batch, even if that item was filtered out.
492    return (batch[-1].version, filtered, len(new_changes) - len(batch))
493
494  def _CopyOverImmutableFields(self, entry):
495    """Preserve immutable fields by copying pre-commit state.
496
497    Args:
498      entry: A sync entity from the client.
499    """
500    if entry.id_string in self._entries:
501      if self._entries[entry.id_string].HasField(
502          'server_defined_unique_tag'):
503        entry.server_defined_unique_tag = (
504            self._entries[entry.id_string].server_defined_unique_tag)
505
506  def _CheckVersionForCommit(self, entry):
507    """Perform an optimistic concurrency check on the version number.
508
509    Clients are only allowed to commit if they report having seen the most
510    recent version of an object.
511
512    Args:
513      entry: A sync entity from the client.  It is assumed that ID fields
514        have been converted to server IDs.
515    Returns:
516      A boolean value indicating whether the client's version matches the
517      newest server version for the given entry.
518    """
519    if entry.id_string in self._entries:
520      # Allow edits/deletes if the version matches, and any undeletion.
521      return (self._entries[entry.id_string].version == entry.version or
522              self._entries[entry.id_string].deleted)
523    else:
524      # Allow unknown ID only if the client thinks it's new too.
525      return entry.version == 0
526
527  def _CheckParentIdForCommit(self, entry):
528    """Check that the parent ID referenced in a SyncEntity actually exists.
529
530    Args:
531      entry: A sync entity from the client.  It is assumed that ID fields
532        have been converted to server IDs.
533    Returns:
534      A boolean value indicating whether the entity's parent ID is an object
535      that actually exists (and is not deleted) in the current account state.
536    """
537    if entry.parent_id_string == ROOT_ID:
538      # This is generally allowed.
539      return True
540    if entry.parent_id_string not in self._entries:
541      print 'Warning: Client sent unknown ID.  Should never happen.'
542      return False
543    if entry.parent_id_string == entry.id_string:
544      print 'Warning: Client sent circular reference.  Should never happen.'
545      return False
546    if self._entries[entry.parent_id_string].deleted:
547      # This can happen in a race condition between two clients.
548      return False
549    if not self._entries[entry.parent_id_string].folder:
550      print 'Warning: Client sent non-folder parent.  Should never happen.'
551      return False
552    return True
553
554  def _RewriteIdsAsServerIds(self, entry, cache_guid, commit_session):
555    """Convert ID fields in a client sync entry to server IDs.
556
557    A commit batch sent by a client may contain new items for which the
558    server has not generated IDs yet.  And within a commit batch, later
559    items are allowed to refer to earlier items.  This method will
560    generate server IDs for new items, as well as rewrite references
561    to items whose server IDs were generated earlier in the batch.
562
563    Args:
564      entry: The client sync entry to modify.
565      cache_guid: The globally unique ID of the client that sent this
566        commit request.
567      commit_session: A dictionary mapping the original IDs to the new server
568        IDs, for any items committed earlier in the batch.
569    """
570    if entry.version == 0:
571      if entry.HasField('client_defined_unique_tag'):
572        # When present, this should determine the item's ID.
573        new_id = self._ClientTagToId(entry.client_defined_unique_tag)
574      else:
575        new_id = self._ClientIdToId(cache_guid, entry.id_string)
576        entry.originator_cache_guid = cache_guid
577        entry.originator_client_item_id = entry.id_string
578      commit_session[entry.id_string] = new_id  # Remember the remapping.
579      entry.id_string = new_id
580    if entry.parent_id_string in commit_session:
581      entry.parent_id_string = commit_session[entry.parent_id_string]
582    if entry.insert_after_item_id in commit_session:
583      entry.insert_after_item_id = commit_session[entry.insert_after_item_id]
584
585  def CommitEntry(self, entry, cache_guid, commit_session):
586    """Attempt to commit one entry to the user's account.
587
588    Args:
589      entry: A SyncEntity protobuf representing desired object changes.
590      cache_guid: A string value uniquely identifying the client; this
591        is used for ID generation and will determine the originator_cache_guid
592        if the entry is new.
593      commit_session: A dictionary mapping client IDs to server IDs for any
594        objects committed earlier this session.  If the entry gets a new ID
595        during commit, the change will be recorded here.
596    Returns:
597      A SyncEntity reflecting the post-commit value of the entry, or None
598      if the entry was not committed due to an error.
599    """
600    entry = DeepCopyOfProto(entry)
601
602    # Generate server IDs for this entry, and write generated server IDs
603    # from earlier entries into the message's fields, as appropriate.  The
604    # ID generation state is stored in 'commit_session'.
605    self._RewriteIdsAsServerIds(entry, cache_guid, commit_session)
606
607    # Perform the optimistic concurrency check on the entry's version number.
608    # Clients are not allowed to commit unless they indicate that they've seen
609    # the most recent version of an object.
610    if not self._CheckVersionForCommit(entry):
611      return None
612
613    # Check the validity of the parent ID; it must exist at this point.
614    # TODO(nick): Implement cycle detection and resolution.
615    if not self._CheckParentIdForCommit(entry):
616      return None
617
618    self._CopyOverImmutableFields(entry);
619
620    # At this point, the commit is definitely going to happen.
621
622    # Deletion works by storing a limited record for an entry, called a
623    # tombstone.  A sync server must track deleted IDs forever, since it does
624    # not keep track of client knowledge (there's no deletion ACK event).
625    if entry.deleted:
626      def MakeTombstone(id_string):
627        """Make a tombstone entry that will replace the entry being deleted.
628
629        Args:
630          id_string: Index of the SyncEntity to be deleted.
631        Returns:
632          A new SyncEntity reflecting the fact that the entry is deleted.
633        """
634        # Only the ID, version and deletion state are preserved on a tombstone.
635        # TODO(nick): Does the production server not preserve the type?  Not
636        # doing so means that tombstones cannot be filtered based on
637        # requested_types at GetUpdates time.
638        tombstone = sync_pb2.SyncEntity()
639        tombstone.id_string = id_string
640        tombstone.deleted = True
641        tombstone.name = ''
642        return tombstone
643
644      def IsChild(child_id):
645        """Check if a SyncEntity is a child of entry, or any of its children.
646
647        Args:
648          child_id: Index of the SyncEntity that is a possible child of entry.
649        Returns:
650          True if it is a child; false otherwise.
651        """
652        if child_id not in self._entries:
653          return False
654        if self._entries[child_id].parent_id_string == entry.id_string:
655          return True
656        return IsChild(self._entries[child_id].parent_id_string)
657
658      # Identify any children entry might have.
659      child_ids = [child.id_string for child in self._entries.itervalues()
660                   if IsChild(child.id_string)]
661
662      # Mark all children that were identified as deleted.
663      for child_id in child_ids:
664        self._SaveEntry(MakeTombstone(child_id))
665
666      # Delete entry itself.
667      entry = MakeTombstone(entry.id_string)
668    else:
669      # Comments in sync.proto detail how the representation of positional
670      # ordering works: the 'insert_after_item_id' field specifies a
671      # predecessor during Commit operations, but the 'position_in_parent'
672      # field provides an absolute ordering in GetUpdates contexts.  Here
673      # we convert from the former to the latter.  Specifically, we'll
674      # generate a numeric position placing the item just after the object
675      # identified by 'insert_after_item_id', and then clear the
676      # 'insert_after_item_id' field so that it's not sent back to the client
677      # during later GetUpdates requests.
678      if entry.HasField('insert_after_item_id'):
679        self._WritePosition(entry, entry.parent_id_string,
680                            entry.insert_after_item_id)
681      else:
682        self._WritePosition(entry, entry.parent_id_string)
683
684    # Preserve the originator info, which the client is not required to send
685    # when updating.
686    base_entry = self._entries.get(entry.id_string)
687    if base_entry and not entry.HasField('originator_cache_guid'):
688      entry.originator_cache_guid = base_entry.originator_cache_guid
689      entry.originator_client_item_id = base_entry.originator_client_item_id
690
691    # Commit the change.  This also updates the version number.
692    self._SaveEntry(entry)
693    return entry
694
695
696class TestServer(object):
697  """An object to handle requests for one (and only one) Chrome Sync account.
698
699  TestServer consumes the sync command messages that are the outermost
700  layers of the protocol, performs the corresponding actions on its
701  SyncDataModel, and constructs an appropropriate response message.
702  """
703
704  def __init__(self):
705    # The implementation supports exactly one account; its state is here.
706    self.account = SyncDataModel()
707    self.account_lock = threading.Lock()
708    # Clients that have talked to us: a map from the full client ID
709    # to its nickname.
710    self.clients = {}
711    self.client_name_generator = ('+' * times + chr(c)
712        for times in xrange(0, sys.maxint) for c in xrange(ord('A'),ord('Z')))
713
714  def GetShortClientName(self, query):
715    parsed = cgi.parse_qs(query[query.find('?')+1:])
716    client_id = parsed.get('client_id')
717    if not client_id:
718      return '?'
719    client_id = client_id[0]
720    if client_id not in self.clients:
721      self.clients[client_id] = self.client_name_generator.next()
722    return self.clients[client_id]
723
724  def HandleCommand(self, query, raw_request):
725    """Decode and handle a sync command from a raw input of bytes.
726
727    This is the main entry point for this class.  It is safe to call this
728    method from multiple threads.
729
730    Args:
731      raw_request: An iterable byte sequence to be interpreted as a sync
732        protocol command.
733    Returns:
734      A tuple (response_code, raw_response); the first value is an HTTP
735      result code, while the second value is a string of bytes which is the
736      serialized reply to the command.
737    """
738    self.account_lock.acquire()
739    try:
740      request = sync_pb2.ClientToServerMessage()
741      request.MergeFromString(raw_request)
742      contents = request.message_contents
743
744      response = sync_pb2.ClientToServerResponse()
745      response.error_code = sync_pb2.ClientToServerResponse.SUCCESS
746      response.store_birthday = self.account.store_birthday
747      log_context = "[Client %s -> %s.py]" % (self.GetShortClientName(query),
748                                              __name__)
749
750      if contents == sync_pb2.ClientToServerMessage.AUTHENTICATE:
751        print '%s Authenticate' % log_context
752        # We accept any authentication token, and support only one account.
753        # TODO(nick): Mock out the GAIA authentication as well; hook up here.
754        response.authenticate.user.email = 'syncjuser@chromium'
755        response.authenticate.user.display_name = 'Sync J User'
756      elif contents == sync_pb2.ClientToServerMessage.COMMIT:
757        print '%s Commit' % log_context
758        self.HandleCommit(request.commit, response.commit)
759      elif contents == sync_pb2.ClientToServerMessage.GET_UPDATES:
760        print ('%s GetUpdates from timestamp %d' %
761               (log_context, request.get_updates.from_timestamp))
762        self.HandleGetUpdates(request.get_updates, response.get_updates)
763      return (200, response.SerializeToString())
764    finally:
765      self.account_lock.release()
766
767  def HandleCommit(self, commit_message, commit_response):
768    """Respond to a Commit request by updating the user's account state.
769
770    Commit attempts stop after the first error, returning a CONFLICT result
771    for any unattempted entries.
772
773    Args:
774      commit_message: A sync_pb.CommitMessage protobuf holding the content
775        of the client's request.
776      commit_response: A sync_pb.CommitResponse protobuf into which a reply
777        to the client request will be written.
778    """
779    commit_response.SetInParent()
780    batch_failure = False
781    session = {}  # Tracks ID renaming during the commit operation.
782    guid = commit_message.cache_guid
783    for entry in commit_message.entries:
784      server_entry = None
785      if not batch_failure:
786        # Try to commit the change to the account.
787        server_entry = self.account.CommitEntry(entry, guid, session)
788
789      # An entryresponse is returned in both success and failure cases.
790      reply = commit_response.entryresponse.add()
791      if not server_entry:
792        reply.response_type = sync_pb2.CommitResponse.CONFLICT
793        reply.error_message = 'Conflict.'
794        batch_failure = True  # One failure halts the batch.
795      else:
796        reply.response_type = sync_pb2.CommitResponse.SUCCESS
797        # These are the properties that the server is allowed to override
798        # during commit; the client wants to know their values at the end
799        # of the operation.
800        reply.id_string = server_entry.id_string
801        if not server_entry.deleted:
802          # Note: the production server doesn't actually send the
803          # parent_id_string on commit responses, so we don't either.
804          reply.position_in_parent = server_entry.position_in_parent
805          reply.version = server_entry.version
806          reply.name = server_entry.name
807          reply.non_unique_name = server_entry.non_unique_name
808        else:
809          reply.version = entry.version + 1
810
811  def HandleGetUpdates(self, update_request, update_response):
812    """Respond to a GetUpdates request by querying the user's account.
813
814    Args:
815      update_request: A sync_pb.GetUpdatesMessage protobuf holding the content
816        of the client's request.
817      update_response: A sync_pb.GetUpdatesResponse protobuf into which a reply
818        to the client request will be written.
819    """
820    update_response.SetInParent()
821    update_sieve = UpdateSieve(update_request)
822    new_timestamp, entries, remaining = self.account.GetChanges(update_sieve)
823
824    update_response.changes_remaining = remaining
825    for entry in entries:
826      reply = update_response.entries.add()
827      reply.CopyFrom(entry)
828    update_sieve.SaveProgress(new_timestamp, update_response)
829