Changeset 2053

Show
Ignore:
Timestamp:
05/27/07 04:34:59 (19 months ago)
Author:
abaumann
Message:

change character encoding used to talk to the phone to UCS2
this makes debug output less legible, but should fix all the problems with strange characters and with line-breaks in strings mucking up my stupid parser

also change mototool backup format to use utf8 and properly escape " and \n characters in saved data

update motosync to minor changes in XML schemas, not yet tested

automatic whitespace cleanup

Location:
plugins/moto-sync
Files:
2 modified

Legend:

Unmodified
Added
Removed
  • plugins/moto-sync/motosync.py

    r1809 r2053  
    266266    exception dates, exception rules, and the event's date/time, 
    267267    returns the motorola recurrence type and a list of exceptions. 
    268      
     268 
    269269    The general approach we take is: if the recursion can be represented 
    270270    by the phone's data structure, we convert it, otherwise we ignore the 
    271271    rule completely (to avoid the event showing up at incorrect times). 
    272      
     272 
    273273    FIXME: this code doesn't yet handle all the tricky parts of RRULE 
    274274    specifications (such as BYSETPOS) 
    275275    """ 
    276      
     276 
    277277    # XXX FIXME: whole function needs reworkign for new XML format 
    278278    return (MOTO_REPEAT_NONE, []) 
    279      
     279 
    280280    # can't support multiple rules 
    281281    if len(rulenodes) != 1: 
     
    397397class PhoneComms: 
    398398    """Functions for directly accessing the phone. 
    399      
     399 
    400400    "device" may be either a path to a local device node, or a bluetooth MAC. 
    401401    """ 
     
    415415    def connect(self): 
    416416        """Connect to the phone and initiate communication. 
    417          
     417 
    418418        Returns True on success, False if already connected. 
    419419        """ 
     
    445445        self.__do_cmd('ATE0Q0V1')  # echo off, result codes off, verbose results 
    446446 
    447         # use ISO 8859-1 encoding for data values, for easier debugging 
    448         # FIXME: change to UCS2? 
    449         self.__do_cmd('AT+CSCS="8859-1"') 
     447        # use UCS2 encoding for data values 
     448        # this is an older version of UTF16 where every char is two bytes long 
     449        # the phone implements it by sending us 2 hex chars per byte, ie. 4 per char 
     450        self.__do_cmd('AT+CSCS="UCS2"') 
    450451 
    451452        (maxevs, numevs, namelen, max_except, _) = self.read_event_params() 
     
    460461        self.contact_data_len = contactlen 
    461462        self.contact_name_len = namelen 
    462          
     463 
    463464        return True 
    464465 
     
    518519    def read_event_params(self): 
    519520        """Read calendar/datebook parameters. 
    520          
     521 
    521522        Returns: maximum number of events, 
    522523                 number of events currently stored, 
     
    565566        exceptions = evdata[-1] 
    566567        data = evdata[:-1] 
    567         placeholders = self.__make_placeholders(data) 
    568         self.__do_cmd(('AT+MDBW=' + placeholders) % tuple(data)) 
     568        # HACK: only the name of the event (data[1]) should be unicode 
     569        for n in range(2, len(data)): 
     570            if type(data[n]) == types.UnicodeType: 
     571                data[n] = data[n].encode('ascii') 
     572        self.__do_cmd('AT+MDBW=' + self.__to_cmd_str(data)) 
    569573        for expos in exceptions: 
    570574            self.__do_cmd('AT+MDBWE=%d,%d,1' % (pos, expos)) 
     
    596600    def read_contact_params(self): 
    597601        """Read phonebook parameters. 
    598          
     602 
    599603        Returns: minimum position, 
    600604                 maximum position, 
     
    647651        """write a single contact to the position specified in the data list""" 
    648652        self.close_calendar() 
    649         placeholders = self.__make_placeholders(data) 
    650         self.__do_cmd(('AT+MPBW=' + placeholders) % tuple(data)) 
     653        # HACK: the email/number and birthday (index 1&23) must not be unicode 
     654        for n in [1, 23]: 
     655            if len(data) > n and type(data[n]) == types.UnicodeType: 
     656                data[n] = data[n].encode('ascii') 
     657        self.__do_cmd('AT+MPBW=' + self.__to_cmd_str(data)) 
    651658 
    652659    def delete_contact(self, pos): 
     
    681688        If it succeeds, return lines as a list; otherwise raise an exception. 
    682689        """ 
    683         cmd = cmd.encode('iso_8859_1') 
    684690        opensync.trace(opensync.TRACE_SENSITIVE, '--> ' + cmd) 
    685691        ret = [] 
     
    728734                valparts = [] 
    729735                for part in parts: 
    730                     if part[0] == '"': 
     736                    if part == '': 
     737                        valparts.append('') 
     738                    elif part[0] == '"': 
    731739                        assert(part[-1] == '"') 
    732                         valparts.append(part[1:-1].decode('iso_8859_1')) 
     740                        valparts.append(part[1:-1]) 
    733741                    elif part[0] == '(': 
    734742                        # parse a range string like '(1-10,45,50-60)' 
     
    739747                            ranges = ranges[0] 
    740748                        valparts.append(ranges) 
     749                    elif len(part) >= 4 and part[0] == '0': 
     750                        # this looks like a UCS2-encoded string 
     751                        valparts.append(part.decode('hex').decode('utf_16_be')) 
    741752                    else: 
    742753                        try: 
     
    748759        return ret 
    749760 
    750     def __make_placeholders(self, vals): 
    751         """Return string expandion placeholders ("%s",%d etc) for a write.""" 
     761    def __to_cmd_str(self, vals): 
     762        """Convert different typed data to a string that can be used in a phone 
     763        write command (ie. MPBW or MDBW.""" 
    752764        def make_placeholder(val): 
    753765            """Return a single placeholder based on the given value's type.""" 
     
    755767            if t == types.IntType: 
    756768                return '%d' 
     769            elif t == types.StringType or val == '': 
     770                return '"%s"' 
     771            elif t == types.UnicodeType: 
     772                return '%s' 
    757773            else: 
    758                 assert(t == types.StringType or t == types.UnicodeType, 'unexpected type %s' % str(t)) 
    759                 return '"%s"' 
    760  
    761         return ','.join(map(make_placeholder, vals)) 
     774                assert(False, 'unexpected type %s' % str(t)) 
     775 
     776        def convert_val(val): 
     777            """Convert a value to an alternate representation, if needed.""" 
     778            if type(val) == types.UnicodeType: 
     779                # convert to the phone's idea of UCS2 
     780                # FIXME: this breaks in the case of surrogate pairs, but I doubt 
     781                # they'll turn up in PIM data, and it's not clear what the phone 
     782                # actually does support as its character set 
     783                return val.encode('utf_16_be').encode('hex').upper() 
     784            else: 
     785                return val 
     786 
     787        return ','.join(map(make_placeholder, vals)) % tuple(map(convert_val, vals)) 
    762788 
    763789 
     
    765791class PhoneEntry: 
    766792    """(abstract) base class representing an event/contact entry 
    767      
     793 
    768794    data is kept roughly as it is stored in the phone 
    769795    """ 
    770796    def __init__(self): 
    771797        pass 
    772      
     798 
    773799    def get_objtype(self): 
    774800        """return the opensync object type string""" 
     
    901927        else: 
    902928            dtstart = self.eventdt.strftime(VCAL_DATE) 
    903             e.setAttribute('DateValue', 'DATE') 
     929            e.setAttribute('Value', 'DATE') 
    904930        appendXMLChild(doc, e, 'Content', dtstart) 
    905931        top.appendChild(e) 
     
    911937        else: 
    912938            dtend = endtime.strftime(VCAL_DATE) 
    913             e.setAttribute('DateValue', 'DATE') 
     939            e.setAttribute('Value', 'DATE') 
    914940        appendXMLChild(doc, e, 'Content', dtend) 
    915941        top.appendChild(e) 
     
    927953        e = doc.createElement('RecurrenceRule') 
    928954        if self.repeat_type == MOTO_REPEAT_DAILY: 
    929             appendXMLChild(doc, e, 'Content', 'DAILY') 
     955            appendXMLChild(doc, e, 'Frequency', 'DAILY') 
    930956        elif self.repeat_type == MOTO_REPEAT_WEEKLY: 
    931             appendXMLChild(doc, e, 'Content', 'WEEKLY') 
     957            appendXMLChild(doc, e, 'Frequency', 'WEEKLY') 
    932958        elif self.repeat_type == MOTO_REPEAT_MONTHLY_DATE: 
    933             appendXMLChild(doc, e, 'Content', 'MONTHLY') 
     959            appendXMLChild(doc, e, 'Frequency', 'MONTHLY') 
    934960            appendXMLChild(doc, e, 'ByMonthDay', str(self.eventdt.day)) 
    935961        elif self.repeat_type == MOTO_REPEAT_MONTHLY_DAY: 
    936             appendXMLChild(doc, e, 'Content', 'MONTHLY') 
     962            appendXMLChild(doc, e, 'Frequency', 'MONTHLY') 
    937963            day = VCAL_DAYS[self.eventdt.weekday()] 
    938964            # compute the week number that the event falls in 
     
    943969            appendXMLChild(doc, e, 'ByDay', '%d%s' % (weeknum, day)) 
    944970        elif self.repeat_type == MOTO_REPEAT_YEARLY: 
    945             appendXMLChild(doc, e, 'Content', 'YEARLY') 
     971            appendXMLChild(doc, e, 'Frequency', 'YEARLY') 
    946972            appendXMLChild(doc, e, 'ByMonth', str(self.eventdt.month)) 
    947973            appendXMLChild(doc, e, 'ByMonthDay', str(self.eventdt.day)) 
     
    9761002            # generate ExceptionDate nodes for them 
    9771003            e = doc.createElement('ExceptionDateTime') 
    978             e.setAttribute('DateValue', 'DATE') 
     1004            e.setAttribute('Value', 'DATE') 
    9791005            for exnum in self.exceptions: 
    9801006                appendXMLChild(doc, e, 'Content', format_time(rrule[exnum], VCAL_DATE)) 
     
    10191045    def __init__(self, data): 
    10201046        """Parse XML event data. 
    1021          
     1047 
    10221048        This function raises UnsupportedDataError for: 
    10231049         * events that recur but can't be represented on the phone 
     
    10651091 
    10661092        # for some reason I don't understand, the phone only allows events 
    1067         # longer than a day if the time flag is set. pander to this by forcing  
     1093        # longer than a day if the time flag is set. pander to this by forcing 
    10681094        # such events to start at midnight local time 
    10691095        if isinstance(self.eventdt, date) and self.duration > timedelta(1): 
     
    12501276class PhoneContactMoto(PhoneContact): 
    12511277    """PhoneContact object for contacts created from phone data. 
    1252      
     1278 
    12531279    Creates a contact with a single child.""" 
    12541280    def __init__(self, data): 
     
    14351461        return (self.pos, self.contact, self.numtype, self.parent.name, 
    14361462                self.contacttype, self.voicetag, self.ringerid, 0, 
    1437                 int(self.primaryflag), self.parent.categorynum,  
     1463                int(self.primaryflag), self.parent.categorynum, 
    14381464                self.profile_icon, self.parent.firstlast_enabled, 
    14391465                self.parent.firstlast_index, self.picture_path, 
     
    14891515class PosAllocator: 
    14901516    """Position-allocator class. 
    1491      
     1517 
    14921518    Remembers which positions in a set are used/free, and allocates new ones. 
    14931519    """ 
     
    15261552class PhoneAccess: 
    15271553    """Grab-bag class of utility functions. 
    1528       
     1554 
    15291555     Interfaces between PhoneComms/PhoneEntry classes and SyncClass below. 
    15301556     """ 
     
    16151641    def delete_entry(self, uid): 
    16161642        """Delete an event with the given UID 
    1617          
     1643 
    16181644        Returns True on success, False otherwise. 
    16191645        """ 
     
    16291655    def update_entry(self, change, context): 
    16301656        """Update an entry or add a new one, from the OSyncChange object. 
    1631          
     1657 
    16321658        Returns True on success, False otherwise. 
    16331659        """ 
     
    16761702        change.uid = self.__generate_uid(entry) 
    16771703        change.hash = self.__gen_hash(entry) 
    1678          
     1704 
    16791705        return True 
    16801706 
     
    17111737    def __uid_to_pos(self, uid): 
    17121738        """Reverse the generate_uid function above. 
    1713          
     1739 
    17141740        Also checks that it is one of ours. 
    17151741        """ 
     
    17181744        lastpos = lastpart.rindex('@') 
    17191745        assert(lastpart[lastpos + 1:] == self.serial[-8:], 'Entry not created on this phone') 
    1720          
     1746 
    17211747        if objtype == "event": 
    17221748            positions = PhoneEvent.unpack_uid(lastpart[:lastpos]) 
  • plugins/moto-sync/mototool

    r1809 r2053  
    5353    """Prompt the user if they are trying to write to the phone.""" 
    5454    if options.mode == 'delete' or options.mode == 'restore': 
    55         print ('WARNING: About to %s all %s entries from the phone!' 
     55        print ('WARNING: About to %s all %s entries on the phone!' 
    5656               % (options.mode, ' & '.join(options.objtype))) 
    5757        print 'Are you sure? [yn] ', 
     
    6767            strings.append(str(val)) 
    6868        else: 
    69             strings.append('"%s"' % val.encode('utf7')) 
    70             assert('"' not in val) 
     69            if val == '': 
     70                strings.append('') 
     71            else: 
     72                s = val.encode('utf8') 
     73                # escape \ to \\, " to \" and newline to \n 
     74                s = s.replace('\\', '\\\\') 
     75                s = s.replace('"', '\\"') 
     76                s = s.replace('\n', '\\n') 
     77                strings.append('"' + s + '"') 
    7178    return ','.join([typestr] + strings) + '\n' 
    7279 
    7380def unpack_backup(line): 
    7481    """reverse the pack_backup function above""" 
     82    # FIXME: is the unescaping sane with arbitrary utf8 characters? 
    7583    if line[-1] == '\n': 
    7684        line = line[:-1] 
     
    7987    wasquote = False 
    8088    inquote = False 
     89    inescape = False 
    8190    for c in line: 
    8291        if c == ',' and not inquote: 
    83             if not wasquote and len(parts) > 0: 
     92            if not wasquote and len(parts) > 0 and nextpart != '': 
    8493                nextpart = int(nextpart) 
     94            if wasquote: 
     95                nextpart = nextpart.decode('utf8') 
    8596            parts.append(nextpart) 
    8697            nextpart = '' 
    8798            wasquote = False 
     99        elif c == '\\' and inquote and not inescape: 
     100            inescape = True 
     101        elif inquote and inescape: 
     102            inescape = False 
     103            if c == 'n': 
     104                c = '\n' 
     105            else: 
     106                assert(c == '"' or c == '\\') 
     107            nextpart = nextpart + c 
    88108        elif c == '"': 
    89109            wasquote = True 
     
    91111        else: 
    92112            nextpart = nextpart + c 
    93     if wasquote: 
    94         parts.append(nextpart.decode('utf7')) 
     113    if wasquote or nextpart == '': 
     114        parts.append(nextpart.decode('utf8')) 
    95115    else: 
    96116        parts.append(int(nextpart))