source: subversion/applications/utils/planetdiff/planetdiff.c @ 5038

Last change on this file since 5038 was 5038, checked in by jonb, 12 years ago

planetdiff: Update to 0.5. Improve efficiency by only escaping values when we are about to output them in the diff

  • Property svn:keywords set to Revision
File size: 21.5 KB
Line 
1/*
2#-----------------------------------------------------------------------------
3# planetdiff - Calculates the differences between 2 planet.osm
4# Roughly equivalent to 'diff' but optimised for planet.osm files
5#
6# Use:
7#      planetdiff planetA.osm planetB.osm > delta-AB.xml
8#
9#      planetpatch planetA.osm delta-AB.xml > planetB.osm
10#
11# One of the input files may come from STDIN by specifying - as the filename
12# Files can be gzip or bzip2 compressed.
13#
14#-----------------------------------------------------------------------------
15# Written by Jon Burgess, Copyright 2007
16#
17# This program is free software; you can redistribute it and/or
18# modify it under the terms of the GNU General Public License
19# as published by the Free Software Foundation; either version 2
20# of the License, or (at your option) any later version.
21#
22# This program is distributed in the hope that it will be useful,
23# but WITHOUT ANY WARRANTY; without even the implied warranty of
24# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25# GNU General Public License for more details.
26#
27# You should have received a copy of the GNU General Public License
28# along with this program; if not, write to the Free Software
29# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
30#-----------------------------------------------------------------------------
31*/
32
33/* TODO:
34 * - Empty!
35 *
36 * DONE:
37 * Escape key/values on output (&"'<>)
38 * write patch tool
39 * Error if file not in required sequence (node/way/relation with increasing ID)
40 * Warn if generator is not planet.rb since others tend to violate ordering above
41 * Generate run-time stats on stderr
42 * Allow direct reading of .gz and .bz2 files
43 * Perform UTF8sanitize on input
44 */
45
46#include <stdio.h>
47#include <unistd.h>
48#include <stdlib.h>
49#include <string.h>
50#include <assert.h>
51
52#include <libxml/xmlstring.h>
53#include <libxml/xmlreader.h>
54
55#include "keyvals.h"
56#include "input.h"
57#include "sanitizer.h"
58
59/* Since {node,way,relation} elements are not nested we can
60 * accumulates the attributes, tags and nodes in global data.
61 * This is maintained independently for planetA and planetB
62 */
63enum type { invalid, node, way, relation, eof } current[2];
64static int osm_id[2];
65static struct keyval tags[2], nds[2], attrs[2];
66static struct keyval member_nodes[2], member_ways[2], member_rels[2];
67static xmlTextReaderPtr reader[2];
68static const char *files[2];
69// Strict mode is considers timestamp and key ordering changes as significant, otherwise they are ignored
70static const int strict = 0;
71// Do we run UTF8sanitizer on input, this should only be necessary if you are using very old planet dumps.
72static const int sanitize = 0;
73
74static int compareTypeID() {
75    // Compare object types
76    if (current[0] < current[1])
77        return -1;
78    if (current[0] > current[1])
79        return +1;
80
81    if (osm_id[0] < osm_id[1])
82        return -1;
83    if (osm_id[0] > osm_id[1])
84        return +1;
85
86    return 0;
87}
88
89static int compareKeyval(struct keyval kv[2])
90{
91    struct keyval *p[2];
92    int r;
93
94    p[0] = kv[0].next;
95    p[1] = kv[1].next;
96#define END(x) (p[x] == &kv[x])
97
98    while(1) {
99        if (END(0) && END(1))
100            return 0;
101        if (END(0))
102            return -1;
103        if (END(1))
104            return +1;
105
106        r = strcmp(p[0]->key, p[1]->key);
107        if (r) return r;
108
109        // Ignore the value of any timstemap= for comparison
110        // This skips formatting and other uninteresting timestamp-only updates
111        // Note: this triggers on both k="timestamp" as well as timestamp=...
112        if (strict || strcmp(p[0]->key, "timestamp")) {
113            r = strcmp(p[0]->value, p[1]->value);
114            if (r) return r;
115        }
116
117        p[0] = p[0]->next;
118        p[1] = p[1]->next;
119    }
120    return 0;
121}
122
123int compareKV(const void *hA, const void *hB)
124{
125    struct keyval * const *a = hA;
126    struct keyval * const *b = hB;
127    int c;
128
129    c = strcmp((*a)->key, (*b)->key);
130    if (c) return c;
131
132    return strcmp((*a)->value, (*b)->value);
133}
134
135void sortList(struct keyval *in, struct keyval *out)
136{
137    struct keyval **t, *p;
138    int len = countList(in);
139    int i;
140
141    initList(out);
142    if (!len)
143        return;
144
145    t = calloc(len, sizeof(*in));
146    assert(t);
147
148    for (i=0, p = in->next; i<len; i++, p = p->next)
149        t[i] = p;
150
151    qsort(t, len, sizeof(*t), compareKV);
152
153    for (i=0; i<len; i++)
154        addItem(out, t[i]->key, t[i]->value, 0);
155
156    free(t);
157}
158
159static int compareLists(struct keyval kv[2])
160{
161    // Strict mode requires all lists to be in a consistent order
162    // non-strict treats the 2 sets of tags below as equivalent
163    //
164    // <k='A' v='1' />
165    // <k='B' v='2' />
166    //
167    // <k='B' v='2' />
168    // <k='A' v='1' />
169
170    struct keyval clone[2];
171    int r;
172
173    if (strict)
174        return compareKeyval(kv);
175
176    if (!listHasData(&kv[0]) && !listHasData(&kv[1]))
177        return 0;
178
179    sortList(&kv[0], &clone[0]);
180    sortList(&kv[1], &clone[1]);
181    r = compareKeyval(clone);
182    resetList(&clone[0]);
183    resetList(&clone[1]);
184
185    return r;
186}
187
188
189static int compareOther(void)
190{
191    int c;
192
193    c = compareKeyval(attrs);
194    if (c) return c;
195
196    c = compareLists(tags);
197    if (c) return c;
198
199    c = compareKeyval(nds);
200    if (c) return c;
201
202    c = compareLists(member_nodes);
203    if (c) return c;
204
205    c = compareLists(member_ways);
206    if (c) return c;
207
208    c = compareLists(member_rels);
209    if (c) return c;
210
211    return 0;
212}
213
214static void collectAttributes(int i)
215{
216    int ret;
217    const xmlChar *name, *value;
218    while ((ret = xmlTextReaderMoveToNextAttribute(reader[i])) == 1) {
219        name  = xmlTextReaderConstName(reader[i]);
220        value = xmlTextReaderConstValue(reader[i]);
221        addItem(&attrs[i], (char *)name, (char *)value, 0);
222        //fprintf(stderr, "%s = %s\n", (char *)name, (char *)value);
223    }
224    if (ret != 0) {
225        fprintf(stderr, "%s : failed to parse attributes\n", files[i]);
226        exit(1);
227    }
228}
229
230static void StartElement(int i, const xmlChar *name)
231{
232    xmlChar *xk, *xv, *xid, *xgen, *xrole, *xtype;
233    static int warn = 1;
234
235    if (xmlStrEqual(name, BAD_CAST "node")) {
236        current[i] = node;
237        collectAttributes(i);
238    } else if (xmlStrEqual(name, BAD_CAST "tag")) {
239        xk = xmlTextReaderGetAttribute(reader[i], BAD_CAST "k");
240        xv = xmlTextReaderGetAttribute(reader[i], BAD_CAST "v");
241        addItem(&tags[i], (char *)xk, (char *)xv, 0);
242        xmlFree(xk);
243        xmlFree(xv);
244    } else if (xmlStrEqual(name, BAD_CAST "member")) {
245        xtype = xmlTextReaderGetAttribute(reader[i], BAD_CAST "type");
246        xid   = xmlTextReaderGetAttribute(reader[i], BAD_CAST "ref");
247        xrole = xmlTextReaderGetAttribute(reader[i], BAD_CAST "role");
248        if (xmlStrEqual(xtype, BAD_CAST "node"))
249            addItem(&member_nodes[i], (char *)xid, (char *)xrole, 0);
250        else if (xmlStrEqual(xtype, BAD_CAST "way"))
251            addItem(&member_ways[i], (char *)xid, (char *)xrole, 0);
252        else if (xmlStrEqual(xtype, BAD_CAST "relation"))
253            addItem(&member_rels[i], (char *)xid, (char *)xrole, 0);
254        else
255            fprintf(stderr, "Unknown type: <member type='%s' ...\n", (char *)xtype);
256        xmlFree(xtype);
257        xmlFree(xid);
258        xmlFree(xrole);
259    } else if (xmlStrEqual(name, BAD_CAST "way")) {
260        current[i] = way;
261        collectAttributes(i);
262    } else if (xmlStrEqual(name, BAD_CAST "nd")) {
263        xid  = xmlTextReaderGetAttribute(reader[i], BAD_CAST "ref");
264        addItem(&nds[i], "ref", (char *)xid, 0);
265        xmlFree(xid);
266    } else if (xmlStrEqual(name, BAD_CAST "relation")) {
267        current[i] = relation;
268        collectAttributes(i);
269    } else if (xmlStrEqual(name, BAD_CAST "osm")) {
270        if (warn) {
271            xgen = xmlTextReaderGetAttribute(reader[i], BAD_CAST "generator");
272            if (!xmlStrEqual(xgen, BAD_CAST "OpenStreetMap planet.rb") && !xmlStrEqual(xgen, BAD_CAST "OpenStreetMap planet.c")) {
273                fprintf(stderr, "Warning: The input file %s was generated by: %s\n", files[i], xgen ? (char *)xgen : "unknown");
274                fprintf(stderr, "this tool relies on the planet.osm format of the format of the planet.osm file\n");
275                fprintf(stderr, "generated by planet.rb of planet.openstreetmap.org and may not work with other osm files.\n\n");
276                warn = 0;
277            }
278            xmlFree(xgen);
279        }
280    } else if (xmlStrEqual(name, BAD_CAST "bound")) {
281        // Ignored
282    } else {
283        fprintf(stderr, "%s: Unknown element name: %s\n", __FUNCTION__, name);
284    }
285}
286
287static int EndElement(int i, const xmlChar *name)
288{
289    // Return 0 for closing tag of node/way/relation
290    int ret;
291    if (xmlStrEqual(name, BAD_CAST "node")) {
292        ret = 0;
293    } else if (xmlStrEqual(name, BAD_CAST "way")) {
294        ret = 0;
295    } else if (xmlStrEqual(name, BAD_CAST "tag")) {
296        ret = 1;
297    } else if (xmlStrEqual(name, BAD_CAST "member")) {
298        ret = 1;
299    } else if (xmlStrEqual(name, BAD_CAST "nd")) {
300        ret = 1;
301    } else if (xmlStrEqual(name, BAD_CAST "relation")) {
302        ret = 0;
303    } else if (xmlStrEqual(name, BAD_CAST "osm")) {
304        ret = 1; // maybe should be 0
305    } else if (xmlStrEqual(name, BAD_CAST "bound")) {
306        ret = 1;
307    } else {
308        fprintf(stderr, "%s: Unknown element name: %s\n", __FUNCTION__, name);
309        ret = 1;
310    }
311    return ret;
312}
313
314// Process an XML node, returns 0 for the closing tag for a node/segment/way
315static int processNode(int i)
316{
317    int ret = 1;
318    int empty;
319    xmlChar *name = xmlTextReaderName(reader[i]);
320    if (name == NULL)
321        name = xmlStrdup(BAD_CAST "--");
322       
323    switch(xmlTextReaderNodeType(reader[i])) {
324        case XML_READER_TYPE_ELEMENT:
325            empty = xmlTextReaderIsEmptyElement(reader[i]);
326            StartElement(i, name);     
327            if (!empty)
328                break;
329            /* Drop through for self closing tags since these generate no end_element */
330        case XML_READER_TYPE_END_ELEMENT:
331            ret = EndElement(i, name);
332            break;
333        case XML_READER_TYPE_SIGNIFICANT_WHITESPACE:
334            /* Ignore */
335            break;
336        default:
337            fprintf(stderr, "Unknown node type %d\n", xmlTextReaderNodeType(reader[i]));
338            break;
339    }
340    xmlFree(name);
341    return ret;
342}
343
344static const char *getName(int i)
345{
346    switch (current[i]) {
347        case node: return "node";
348        case way: return "way";
349        case relation: return "relation";
350        case invalid: return "invalid";
351        default:
352            break;
353    }
354    fprintf(stderr, "Unhandled type %d\n", current[i]);
355    exit(5);
356}
357
358// Reads in a complete OSM object, e.g. <node> to <node/>
359static void getobject(int i)
360{
361    int ret = 1;
362    const char *id;
363    enum type last_type = current[i];
364    const char *last_name = getName(i);
365    int last_id = osm_id[i];
366    //static int count;
367
368    // Delete data for previous object
369    resetList(&attrs[i]);
370    resetList(&nds[i]);
371    resetList(&tags[i]);
372    resetList(&member_nodes[i]);
373    resetList(&member_ways[i]);
374    resetList(&member_rels[i]);
375
376    current[i] = eof;
377    if (!reader[i])
378        return; // EOF
379
380    while(ret == 1) {
381        ret = xmlTextReaderRead(reader[i]);
382        if (ret == 0) {
383            //fprintf(stderr, "EOF %s\n", files[i]);
384            xmlFreeTextReader(reader[i]);
385            reader[i] = NULL;
386            return;
387        }
388        if (ret != 1) {
389            fprintf(stderr, "Error parsing file %s\n", files[i]);
390            exit(3);
391        }
392
393        ret = processNode(i);
394    }
395    // Retrieve osm_id for node/way/relation
396    id = getItem(&attrs[i], "id");
397    osm_id[i] = id ? atoi(id) : 0;
398    //fprintf(stderr, "%d: object %d, id=%d\n", i, current[i], osm_id[i]);
399
400    // Perform sanity checking on element sequence. The output of planet.rb conforms to this and
401    // the current diff/patch algorithm relies on these properties.
402    if (current[i] < last_type) {
403        fprintf(stderr, "Error: <%s> seen after <%s>. The file must be strictly ordered node/way/relation.\n", getName(i), last_name);
404        fprintf(stderr, "The planet.osm generated by the planet export (planet.rb) is consistent with this.\n");
405        exit(8);
406    }
407    if ((current[i] == last_type) && (osm_id[i] < last_id)) {
408        fprintf(stderr, "Error: <%s id=%d> seen after <%s id=%d>. The IDs must be in increasing order.\n", getName(i), osm_id[i], getName(i), last_id);
409        fprintf(stderr, "The planet.osm generated by the planet export (planet.rb) is consistent with this.\n");
410        exit(9);
411    }
412#if 0
413    // Some run-time stats, only count first stream
414    if (i == 0) {
415        count++;
416        if (current[i] != last_type) {
417            count = 0;
418            fprintf(stderr, "\n");
419        }
420        if ((count % 10000) == 0)
421            fprintf(stderr, "\rProcessing: %s(%dk)", getName(i), count/1000);
422    }
423#endif
424}
425
426
427static void displayNode(int i, const char *indent)
428{
429    struct keyval *p;
430
431    printf("%s<%s", indent,getName(i));
432
433    while ((p = popItem(&attrs[i])) != NULL) {
434        printf(" %s=\"%s\"", p->key, p->value);
435        freeItem(p);
436    }
437
438    if (listHasData(&tags[i]) || listHasData(&nds[i]) || listHasData(&member_nodes[i]) ||
439        listHasData(&member_ways[i]) || listHasData(&member_rels[i])) {
440        printf(">\n");
441
442        while ((p = popItem(&nds[i])) != NULL) {
443            printf("%s  <nd ref=\"%s\" />\n", indent, p->value);
444            freeItem(p);
445        }
446        while ((p = popItem(&member_nodes[i])) != NULL) {
447            printf("%s  <member type=\"node\" ref=\"%s\" role=\"%s\" />\n", indent, p->key, p->value);
448            freeItem(p);
449        }
450        while ((p = popItem(&member_ways[i])) != NULL) {
451            printf("%s  <member type=\"way\" ref=\"%s\" role=\"%s\" />\n", indent, p->key, p->value);
452            freeItem(p);
453        }
454        while ((p = popItem(&member_rels[i])) != NULL) {
455            printf("%s  <member type=\"relation\" ref=\"%s\" role=\"%s\" />\n", indent, p->key, p->value);
456            freeItem(p);
457        }
458        while ((p = popItem(&tags[i])) != NULL) {
459            printf("%s  <tag k=\"%s\" v=\"%s\" />\n", indent, p->key, p->value);
460            freeItem(p);
461        }
462        printf("%s</%s>\n", indent,getName(i));
463    } else {
464        printf("/>\n");
465    }
466}
467
468static void displayDiff(int i)
469{
470    const char *operation = i ? "add" : "delete";
471    printf("  <%s>\n", operation);
472    displayNode(i, "    ");
473    printf("  </%s>\n", operation);
474}
475
476static void displayPatch(int i)
477{
478    displayNode(i, "  ");
479}
480
481
482static void process(void)
483{
484    int c;
485
486    getobject(0);
487    getobject(1);
488
489    while (reader[0] || reader[1]) {
490        c = compareTypeID();
491        if (c == 0) {
492            // Matching object type and ID, generate diff if content differs
493            if (compareOther()) {
494                displayDiff(0);
495                displayDiff(1);
496            }
497            getobject(0);
498            getobject(1);
499        } else if (c < 0) {
500            // Object in stream 0 is missing in 1, generate diff to remove old content 0
501            displayDiff(0);
502            getobject(0);
503        } else {
504            // Object in stream 0 is ahead of 1, generate diff to add content in 1
505            displayDiff(1);
506            getobject(1);
507        }
508    }
509}
510
511
512
513static int diffFiles(void)
514{
515    int i;
516
517    for(i=0; i<2; i++) {
518        if (sanitize)
519            reader[i] = sanitizerOpen(files[i]);
520        else
521            reader[i] = inputUTF8(files[i]);
522        if (!reader[i]) {
523            fprintf(stderr, "Unable to open %s\n", files[i]);
524            return 1;
525        }
526    }
527
528    printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
529    printf("<planetdiff version=\"0.5\" generator=\"OpenStreetMap planetdiff\" from=\"%s\" to=\"%s\">\n", files[0], files[1]);
530    process();
531    printf("</planetdiff>\n");
532
533    return 0;
534}
535
536
537static void applyPatch(int mode)
538{
539    int c;
540    //fprintf(stdout,"%s: %d id=%d\n", (mode > 0)? "add" : "delete", current[1], osm_id[1]);
541
542    // Stream input until we get to or pass the matching type+id
543    while ((c = compareTypeID()) < 0) {
544        if (current[0] != invalid)
545            displayPatch(0);
546        getobject(0);
547    }
548    if (mode == -1) {
549        if ((c == 0) && (compareOther() == 0)) {
550            // Perfect match, remove this from input by fetching next object
551            getobject(0);
552            return;
553        }
554        fprintf(stderr, "Error remove for <%s id=%d> does not match input file. %s\n",
555                getName(1), osm_id[1], (c==0)?"Object content differs.":"Object ID missing.");
556        exit(6);
557    } else if (mode == +1) {
558        if (c > 0) {
559            // Found next input following the insert, emit the new content but not input (yet)
560            displayPatch(1);
561            return;
562        }
563        fprintf(stderr, "Error adding <%s id=%d> input file already contains this object\n", getName(1), osm_id[1]);
564        exit(6);
565    }
566}
567
568static void processPatchNode(void)
569{
570    int i = 1; // Patch file is always stream 1
571    int empty;
572    int mode = 0; // -1 = remove, +1 = add;
573    xmlChar *name = xmlTextReaderName(reader[i]);
574    if (name == NULL)
575        name = xmlStrdup(BAD_CAST "--");
576       
577    switch(xmlTextReaderNodeType(reader[i])) {
578        case XML_READER_TYPE_ELEMENT:
579            empty = xmlTextReaderIsEmptyElement(reader[i]);
580            if (xmlStrEqual(name, BAD_CAST "add"))
581                mode = +1;
582            else if (xmlStrEqual(name, BAD_CAST "delete"))
583                mode = -1;
584            else if (xmlStrEqual(name, BAD_CAST "planetdiff")) {
585                mode = 0;
586                break;
587            } else {
588                fprintf(stderr, "Unexpected node: <%s>\n", (char *)name);
589                if (xmlStrEqual(name, BAD_CAST "osm"))
590                    fprintf(stderr, "It looks like %s is an osm file not a planetdiff\n", files[i]);
591                exit(1);
592            }
593            // Retrieve the object to add/remove from the patch file and apply
594            if (!empty) {
595                getobject(1);
596                applyPatch(mode);
597                break;
598            }
599            /* Drop through for self closing tags since these generate no end_element */
600        case XML_READER_TYPE_END_ELEMENT:
601            mode = 0;
602            break;
603        case XML_READER_TYPE_SIGNIFICANT_WHITESPACE:
604            /* Ignore */
605            break;
606        default:
607            fprintf(stderr, "Unknown node type %d\n", xmlTextReaderNodeType(reader[i]));
608            break;
609    }
610    xmlFree(name);
611}
612
613
614static int patchFiles(void)
615{
616    int ret = 0;
617    int i;
618    // files[0] = planet.osm (input)
619    // files[1] = delta.xml (patch)
620    for(i=0; i<2; i++) {
621        if (sanitize)
622            reader[i] = sanitizerOpen(files[i]);
623        else
624            reader[i] = inputUTF8(files[i]);
625        if (!reader[i]) {
626            fprintf(stderr, "Unable to open %s\n", files[i]);
627            return 1;
628        }
629    }
630
631    printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
632    printf("<osm version=\"0.5\" generator=\"OpenStreetMap planet.rb\">\n");
633    // Ensure generator string length matches original planet.rb
634    // to allow 'cmp -l' to be run on output to verify correctness
635    //printf("<osm version=\"0.5\" generator=\"OSM planetpatch tool---\">\n");
636
637    ret = xmlTextReaderRead(reader[1]);
638
639    while (ret == 1) {
640        processPatchNode();
641        ret = xmlTextReaderRead(reader[1]);
642    }
643
644    if (ret != 0) {
645        fprintf(stderr, "%s : failed to parse patch\n", files[1]);
646        return ret;
647    }
648
649    // Displaying any trailer from input
650    while ((current[0] != eof) && reader[0]) {
651        displayPatch(0);
652        getobject(0);
653    }
654
655    printf("</osm>\n");
656
657    xmlFreeTextReader(reader[0]);
658    xmlFreeTextReader(reader[1]);
659    return 0;
660}
661
662
663static void usageDiff(const char *arg0)
664{
665    fprintf(stderr, "Usage:\n\t%s planet1.osm planet2.osm > planet.diff\n\nGenerates a difference file between the two input planet.osm files.\nThis binary is based on SVN revision: $Rev: 5038 $.\n", arg0);
666    exit(1);
667}
668
669static int mainDiff(int argc, char *argv[], const char *name)
670{
671    int i;
672
673    if (argc != 3) {
674        usageDiff(name);
675        exit(1);
676    }
677
678    for (i=0; i<2; i++) {
679        files[i] = argv[i+1];
680        initList(&tags[i]);
681        initList(&nds[i]);
682        initList(&attrs[i]);
683        initList(&member_nodes[i]);
684        initList(&member_ways[i]);
685        initList(&member_rels[i]);
686    }
687
688    if (diffFiles() != 0)
689        exit(2);
690
691    return 0;
692}
693
694static void usagePatch(const char *arg0)
695{
696    fprintf(stderr, "Usage:\n\t%s planet.osm delta.xml > out.osm\n\n", arg0);
697    fprintf(stderr, "Applies the differences listed in delta.xml to the input planet.osm\n");
698    exit(1);
699}
700
701static int mainPatch(int argc, char *argv[], const char *name)
702{
703    int i;
704
705    if (argc != 3) {
706        usagePatch(name);
707        exit(1);
708    }
709
710    for (i=0; i<2; i++) {
711        files[i] = argv[i+1];
712        initList(&tags[i]);
713        initList(&nds[i]);
714        initList(&attrs[i]);
715        initList(&member_nodes[i]);
716        initList(&member_ways[i]);
717        initList(&member_rels[i]);
718    }
719
720    if (patchFiles() != 0)
721        exit(2);
722
723    return 0;
724}
725
726int main(int argc, char *argv[])
727{
728    int r;
729    const char *name;
730    LIBXML_TEST_VERSION
731
732    name = strrchr(argv[0], '/');
733
734    if (!name)
735        name = strrchr(argv[0], '\\');
736
737    if (name)
738        name++;
739    else
740        name = argv[0];
741
742    if (!strcmp(name, "planetdiff"))
743            r = mainDiff(argc, argv, name);
744    else if (!strcmp(name, "planetpatch"))
745            r = mainPatch(argc, argv, name);
746    else {
747        fprintf(stderr, "Usage error - should be called as planetdiff or planetpatch (not '%s')\n", name);
748        exit(1);
749    }
750
751    xmlCleanupParser();
752    xmlMemoryDump();
753    fprintf(stderr, "\n");
754    return r;
755}
Note: See TracBrowser for help on using the repository browser.