Fix crash in record conv rule select YAZ-812
[yaz-moved-to-github.git] / test / test_record_conv.c
1 /* This file is part of the YAZ toolkit.
2  * Copyright (C) Index Data
3  * See the file LICENSE for details.
4  */
5 #if HAVE_CONFIG_H
6 #include <config.h>
7 #endif
8
9 #include <yaz/record_conv.h>
10 #include <yaz/test.h>
11 #include <yaz/wrbuf.h>
12 #include <string.h>
13 #include <yaz/log.h>
14 #include <yaz/proto.h>
15 #include <yaz/prt-ext.h>
16 #include <yaz/oid_db.h>
17 #if YAZ_HAVE_XML2
18
19 #include <libxml/parser.h>
20 #include <libxml/tree.h>
21
22 #if YAZ_HAVE_XSLT
23 #include <libxslt/xslt.h>
24 #endif
25
26 yaz_record_conv_t conv_configure(const char *xmlstring, WRBUF w)
27 {
28     xmlDocPtr doc = xmlParseMemory(xmlstring, strlen(xmlstring));
29     if (!doc)
30     {
31         wrbuf_printf(w, "xmlParseMemory");
32         return 0;
33     }
34     else
35     {
36         xmlNodePtr ptr = xmlDocGetRootElement(doc);
37         yaz_record_conv_t p = yaz_record_conv_create();
38
39         if (p)
40         {
41             const char *srcdir = getenv("srcdir");
42             if (srcdir)
43                 yaz_record_conv_set_path(p, srcdir);
44         }
45         if (!ptr)
46         {
47             wrbuf_printf(w, "xmlDocGetRootElement");
48             yaz_record_conv_destroy(p);
49             p = 0;
50         }
51         else if (!p)
52         {
53             wrbuf_printf(w, "yaz_record_conv_create");
54         }
55         else
56         {
57
58
59             int r = yaz_record_conv_configure(p, ptr);
60
61             if (r)
62             {
63                 wrbuf_puts(w, yaz_record_conv_get_error(p));
64                 yaz_record_conv_destroy(p);
65                 p = 0;
66             }
67         }
68         xmlFreeDoc(doc);
69         return p;
70     }
71 }
72
73 int conv_configure_test(const char *xmlstring, const char *expect_error,
74                         yaz_record_conv_t *pt)
75 {
76     WRBUF w = wrbuf_alloc();
77     int ret;
78
79     yaz_record_conv_t p = conv_configure(xmlstring, w);
80
81     if (!p)
82     {
83         if (expect_error && !strcmp(wrbuf_cstr(w), expect_error))
84             ret = 1;
85         else
86         {
87             ret = 0;
88             printf("%s\n", wrbuf_cstr(w));
89         }
90     }
91     else
92     {
93         if (expect_error)
94             ret = 0;
95         else
96             ret = 1;
97     }
98
99     if (pt)
100         *pt = p;
101     else
102         if (p)
103             yaz_record_conv_destroy(p);
104
105     wrbuf_destroy(w);
106     return ret;
107 }
108
109 static void tst_configure(void)
110 {
111
112
113
114     YAZ_CHECK(conv_configure_test("<bad", "xmlParseMemory", 0));
115
116
117     YAZ_CHECK(conv_configure_test("<backend syntax='usmarc' name='F'>"
118                                   "<bad/></backend>",
119                                   "Element <backend>: expected <marc> or "
120                                   "<xslt> element, got <bad>", 0));
121
122 #if YAZ_HAVE_XSLT
123     YAZ_CHECK(conv_configure_test("<backend syntax='usmarc' name='F'>"
124                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
125                                   "<marc"
126                                   " inputcharset=\"marc-8\""
127                                   " outputcharset=\"marc-8\""
128                                   "/>"
129                                   "</backend>",
130                                   "Element <marc>: attribute 'inputformat' "
131                                   "required", 0));
132     YAZ_CHECK(conv_configure_test("<backend syntax='usmarc' name='F'>"
133                                   "<xslt/>"
134                                   "</backend>",
135                                   "Element <xslt>: attribute 'stylesheet' "
136                                   "expected", 0));
137     YAZ_CHECK(conv_configure_test("<backend syntax='usmarc' name='F'>"
138                                   "<marc"
139                                   " inputcharset=\"utf-8\""
140                                   " outputcharset=\"marc-8\""
141                                   " inputformat=\"xml\""
142                                   " outputformat=\"marc\""
143                                   "/>"
144                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
145                                   "</backend>",
146                                   0, 0));
147 #else
148     YAZ_CHECK(conv_configure_test("<backend syntax='usmarc' name='F'>"
149                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
150                                   "</backend>",
151                                   "xslt unsupported."
152                                   " YAZ compiled without XSLT support", 0));
153 #endif
154 }
155
156 static int conv_convert_test(yaz_record_conv_t p,
157                              const char *input_record,
158                              const char *output_expect_record)
159 {
160     int ret = 0;
161     if (!p)
162     {
163         YAZ_CHECK(ret);
164     }
165     else
166     {
167         WRBUF output_record = wrbuf_alloc();
168         int r = yaz_record_conv_record(p, input_record, strlen(input_record),
169                                        output_record);
170         if (r)
171         {
172             if (output_expect_record)
173             {
174                 printf("yaz_record_conv error=%s\n",
175                        yaz_record_conv_get_error(p));
176                 ret = 0;
177             }
178             else
179                 ret = 1;
180         }
181         else
182         {
183             if (!output_expect_record)
184             {
185                 ret = 0;
186             }
187             else if (strcmp(output_expect_record, wrbuf_cstr(output_record)))
188             {
189                 ret = 0;
190                 printf("got-output_record len=%ld: %s\n",
191                        (long) wrbuf_len(output_record),
192                        wrbuf_cstr(output_record));
193                 printf("output_expect_record len=%ld %s\n",
194                        (long) strlen(output_expect_record),
195                        output_expect_record);
196             }
197             else
198             {
199                 ret = 1;
200             }
201         }
202         wrbuf_destroy(output_record);
203     }
204     return ret;
205 }
206
207 static int conv_convert_test_iter(yaz_record_conv_t p,
208                                   const char *input_record,
209                                   const char *output_expect_record,
210                                   int num_iter)
211 {
212     int i;
213     int ret;
214     for (i = 0; i < num_iter; i++)
215     {
216         ret = conv_convert_test(p, input_record, output_expect_record);
217         if (!ret)
218             break;
219     }
220     return ret;
221 }
222
223 static void tst_convert1(void)
224 {
225     yaz_record_conv_t p = 0;
226     const char *marcxml_rec =
227         "<record xmlns=\"http://www.loc.gov/MARC21/slim\">\n"
228         "  <leader>00080nam a22000498a 4500</leader>\n"
229         "  <controlfield tag=\"001\">   11224466 </controlfield>\n"
230         "  <datafield tag=\"010\" ind1=\" \" ind2=\" \">\n"
231         "    <subfield code=\"a\">   11224466 </subfield>\n"
232         "  </datafield>\n"
233         "</record>\n";
234     const char *tmarcxml_rec =
235         "<r xmlns=\"http://www.indexdata.com/MARC21/turboxml\">\n"
236         "  <l>00080nam a22000498a 4500</l>\n"
237         "  <c001>   11224466 </c001>\n"
238         "  <d010 i1=\" \" i2=\" \">\n"
239         "    <sa>   11224466 </sa>\n"
240         "  </d010>\n"
241         "</r>\n";
242     const char *iso2709_rec =
243         "\x30\x30\x30\x38\x30\x6E\x61\x6D\x20\x61\x32\x32\x30\x30\x30\x34"
244         "\x39\x38\x61\x20\x34\x35\x30\x30\x30\x30\x31\x30\x30\x31\x33\x30"
245         "\x30\x30\x30\x30\x30\x31\x30\x30\x30\x31\x37\x30\x30\x30\x31\x33"
246         "\x1E\x20\x20\x20\x31\x31\x32\x32\x34\x34\x36\x36\x20\x1E\x20\x20"
247         "\x1F\x61\x20\x20\x20\x31\x31\x32\x32\x34\x34\x36\x36\x20\x1E\x1D";
248
249     const char *solrmarc_rec =
250         "\x30\x30\x30\x38\x30\x6E\x61\x6D\x20\x61\x32\x32\x30\x30\x30\x34"
251         "\x39\x38\x61\x20\x34\x35\x30\x30\x30\x30\x31\x30\x30\x31\x33\x30"
252         "\x30\x30\x30\x30\x30\x31\x30\x30\x30\x31\x37\x30\x30\x30\x31\x33"
253         "#30;\x20\x20\x20\x31\x31\x32\x32\x34\x34\x36\x36\x20#30;\x20\x20"
254         "#31;\x61\x20\x20\x20\x31\x31\x32\x32\x34\x34\x36\x36\x20#30;#29;";
255     const char *raw_rec = /* raw is xml-string of marcxml_rec */
256         "<raw>&lt;record xmlns=\"http://www.loc.gov/MARC21/slim\">\n"
257         "  &lt;leader>00080nam a22000498a 4500&lt;/leader>\n"
258         "  &lt;controlfield tag=\"001\">   11224466 &lt;/controlfield>\n"
259         "  &lt;datafield tag=\"010\" ind1=\" \" ind2=\" \">\n"
260         "    &lt;subfield code=\"a\">   11224466 &lt;/subfield>\n"
261         "  &lt;/datafield>\n"
262         "&lt;/record>\n</raw>\n";
263
264     YAZ_CHECK(conv_configure_test("<backend>"
265                                   "<marc"
266                                   " inputcharset=\"utf-8\""
267                                   " outputcharset=\"marc-8\""
268                                   " inputformat=\"xml\""
269                                   " outputformat=\"marc\""
270                                   "/>"
271                                   "</backend>",
272                                   0, &p));
273     YAZ_CHECK(conv_convert_test(p, marcxml_rec, iso2709_rec));
274     YAZ_CHECK(conv_convert_test(p, tmarcxml_rec, iso2709_rec));
275     yaz_record_conv_destroy(p);
276
277     YAZ_CHECK(conv_configure_test("<backend>"
278                                   "<marc"
279                                   " outputcharset=\"utf-8\""
280                                   " inputcharset=\"marc-8\""
281                                   " outputformat=\"marcxml\""
282                                   " inputformat=\"marc\""
283                                   "/>"
284                                   "</backend>",
285                                   0, &p));
286     YAZ_CHECK(conv_convert_test(p, iso2709_rec, marcxml_rec));
287     yaz_record_conv_destroy(p);
288
289     YAZ_CHECK(conv_configure_test("<backend>"
290                                   "<solrmarc/>"
291                                   "<marc"
292                                   " outputcharset=\"utf-8\""
293                                   " inputcharset=\"marc-8\""
294                                   " outputformat=\"marcxml\""
295                                   " inputformat=\"marc\""
296                                   "/>"
297                                   "</backend>",
298                                   0, &p));
299     YAZ_CHECK(conv_convert_test(p, solrmarc_rec, marcxml_rec));
300     yaz_record_conv_destroy(p);
301
302     YAZ_CHECK(conv_configure_test("<backend>"
303                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
304                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
305                                   "<marc"
306                                   " inputcharset=\"utf-8\""
307                                   " outputcharset=\"marc-8\""
308                                   " inputformat=\"xml\""
309                                   " outputformat=\"marc\""
310                                   "/>"
311                                   "<marc"
312                                   " outputcharset=\"utf-8\""
313                                   " inputcharset=\"marc-8\""
314                                   " outputformat=\"marcxml\""
315                                   " inputformat=\"marc\""
316                                   "/>"
317                                   "</backend>",
318                                   0, &p));
319     YAZ_CHECK(conv_convert_test(p, marcxml_rec, marcxml_rec));
320     yaz_record_conv_destroy(p);
321
322
323     YAZ_CHECK(conv_configure_test("<backend>"
324                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
325                                   "<xslt stylesheet=\"test_record_conv.xsl\"/>"
326                                   "<marc"
327                                   " outputcharset=\"marc-8\""
328                                   " inputformat=\"xml\""
329                                   " outputformat=\"marc\""
330                                   "/>"
331                                   "<marc"
332                                   " inputcharset=\"marc-8\""
333                                   " outputformat=\"marcxml\""
334                                   " inputformat=\"marc\""
335                                   "/>"
336                                   "</backend>",
337                                   0, &p));
338     YAZ_CHECK(conv_convert_test(p, marcxml_rec, marcxml_rec));
339     yaz_record_conv_destroy(p);
340
341     YAZ_CHECK(conv_configure_test("<backend>"
342                                   "<select path=\"/raw\"/>"
343                                   "</backend>",
344                                   0, &p));
345     YAZ_CHECK(conv_convert_test(p, raw_rec, marcxml_rec));
346     yaz_record_conv_destroy(p);
347 }
348
349 static void tst_convert2(void)
350 {
351     yaz_record_conv_t p = 0;
352     const char *marcxml_rec =
353         "<record xmlns=\"http://www.loc.gov/MARC21/slim\">\n"
354         "  <leader>00080nam a22000498a 4500</leader>\n"
355         "  <controlfield tag=\"001\">   11224466 </controlfield>\n"
356         "  <datafield tag=\"010\" ind1=\" \" ind2=\" \">\n"
357         "    <subfield code=\"a\">k&#xf8;benhavn</subfield>\n"
358         "  </datafield>\n"
359         "</record>\n";
360     const char *iso2709_rec =
361         "\x30\x30\x30\x37\x37\x6E\x61\x6D\x20\x61\x32\x32\x30\x30\x30\x34"
362         "\x39\x38\x61\x20\x34\x35\x30\x30\x30\x30\x31\x30\x30\x31\x33\x30"
363         "\x30\x30\x30\x30\x30\x31\x30\x30\x30\x31\x34\x30\x30\x30\x31\x33"
364         "\x1E\x20\x20\x20\x31\x31\x32\x32\x34\x34\x36\x36\x20\x1E\x20\x20"
365         "\x1F\x61\x6b\xb2\x62\x65\x6e\x68\x61\x76\x6e\x1E\x1D";
366
367     YAZ_CHECK(conv_configure_test("<backend>"
368                                   "<marc"
369                                   " inputcharset=\"utf-8\""
370                                   " outputcharset=\"marc-8\""
371                                   " inputformat=\"xml\""
372                                   " outputformat=\"marc\""
373                                   "/>"
374                                   "</backend>",
375                                   0, &p));
376     YAZ_CHECK(conv_convert_test_iter(p, marcxml_rec, iso2709_rec, 100));
377     yaz_record_conv_destroy(p);
378 }
379
380 static void tst_convert3(void)
381 {
382     NMEM nmem = nmem_create();
383     int ret;
384     yaz_record_conv_t p = 0;
385
386     const char *iso2709_rec =
387         "\x30\x30\x30\x37\x37\x6E\x61\x6D\x20\x20\x32\x32\x30\x30\x30\x34"
388         "\x39\x38\x61\x20\x34\x35\x30\x30\x30\x30\x31\x30\x30\x31\x33\x30"
389         "\x30\x30\x30\x30\x30\x31\x30\x30\x30\x31\x34\x30\x30\x30\x31\x33"
390         "\x1E\x20\x20\x20\x31\x31\x32\x32\x34\x34\x36\x36\x20\x1E\x20\x20"
391         "\x1F\x61\x6b\xb2\x62\x65\x6e\x68\x61\x76\x6e\x1E\x1D";
392
393     const char *opacxml_rec =
394         "<opacRecord>\n"
395         "  <bibliographicRecord>\n"
396         "<record xmlns=\"http://www.loc.gov/MARC21/slim\">\n"
397         "  <leader>00077nam a22000498a 4500</leader>\n"
398         "  <controlfield tag=\"001\">   11224466 </controlfield>\n"
399         "  <datafield tag=\"010\" ind1=\" \" ind2=\" \">\n"
400         "    <subfield code=\"a\">k" "\xc3" "\xb8" /* oslash in UTF_8 */
401         "benhavn</subfield>\n"
402         "  </datafield>\n"
403         "</record>\n"
404         "  </bibliographicRecord>\n"
405         "<holdings>\n"
406         " <holding>\n"
407         "  <typeOfRecord>u</typeOfRecord>\n"
408         "  <encodingLevel>U</encodingLevel>\n"
409         "  <receiptAcqStatus>0</receiptAcqStatus>\n"
410         "  <dateOfReport>000000</dateOfReport>\n"
411         "  <nucCode>s-FM/GC</nucCode>\n"
412         "  <localLocation>Main or Science/Business Reading Rms - STORED OFFSITE</localLocation>\n"
413         "  <callNumber>MLCM 89/00602 (N)</callNumber>\n"
414         "  <shelvingData>FT MEADE</shelvingData>\n"
415         "  <copyNumber>Copy 1</copyNumber>\n"
416         "  <volumes>\n"
417         "   <volume>\n"
418         "    <enumeration>1</enumeration>\n"
419         "    <chronology>2</chronology>\n"
420         "    <enumAndChron>3</enumAndChron>\n"
421         "   </volume>\n"
422         "   <volume>\n"
423         "    <enumeration>1</enumeration>\n"
424         "    <chronology>2</chronology>\n"
425         "    <enumAndChron>3</enumAndChron>\n"
426         "   </volume>\n"
427         "  </volumes>\n"
428         "  <circulations>\n"
429         "   <circulation>\n"
430         "    <availableNow value=\"1\"/>\n"
431         "    <availabilityDate>20130129</availabilityDate>\n"
432         "    <itemId>1226176</itemId>\n"
433         "    <renewable value=\"0\"/>\n"
434         "    <onHold value=\"0\"/>\n"
435         "   </circulation>\n"
436         "  </circulations>\n"
437         " </holding>\n"
438         "</holdings>\n"
439         "</opacRecord>\n";
440
441     Z_OPACRecord *z_opac = nmem_malloc(nmem, sizeof(*z_opac));
442     Z_HoldingsAndCircData *h;
443     Z_CircRecord *circ;
444
445     z_opac->bibliographicRecord =
446         z_ext_record_oid_nmem(nmem, yaz_oid_recsyn_usmarc,
447                               iso2709_rec, strlen(iso2709_rec));
448     z_opac->num_holdingsData = 1;
449     z_opac->holdingsData = (Z_HoldingsRecord **)
450         nmem_malloc(nmem, sizeof(Z_HoldingsRecord *) * 1);
451     z_opac->holdingsData[0] = (Z_HoldingsRecord *)
452         nmem_malloc(nmem, sizeof(Z_HoldingsRecord));
453     z_opac->holdingsData[0]->which = Z_HoldingsRecord_holdingsAndCirc;
454     h = z_opac->holdingsData[0]->u.holdingsAndCirc = (Z_HoldingsAndCircData *)
455          nmem_malloc(nmem, sizeof(*h));
456     h->typeOfRecord = nmem_strdup(nmem, "u");
457     h->encodingLevel = nmem_strdup(nmem, "U");
458     h->format = 0;
459     h->receiptAcqStatus = nmem_strdup(nmem, "0");
460     h->generalRetention = 0;
461     h->completeness = 0;
462     h->dateOfReport = nmem_strdup(nmem, "000000");
463     h->nucCode = nmem_strdup(nmem, "s-FM/GC");
464     h->localLocation = nmem_strdup(nmem,
465                                    "Main or Science/Business Reading "
466                                    "Rms - STORED OFFSITE");
467     h->shelvingLocation = 0;
468     h->callNumber = nmem_strdup(nmem, "MLCM 89/00602 (N)");
469     h->shelvingData = nmem_strdup(nmem, "FT MEADE");
470     h->copyNumber = nmem_strdup(nmem, "Copy 1");
471     h->publicNote = 0;
472     h->reproductionNote = 0;
473     h->termsUseRepro = 0;
474     h->enumAndChron = 0;
475     h->num_volumes = 2;
476     h->volumes = 0;
477
478     h->volumes = (Z_Volume **)
479         nmem_malloc(nmem, 2 * sizeof(Z_Volume *));
480
481     h->volumes[0] = (Z_Volume *)
482         nmem_malloc(nmem, sizeof(Z_Volume));
483     h->volumes[1] = h->volumes[0];
484
485     h->volumes[0]->enumeration = nmem_strdup(nmem, "1");
486     h->volumes[0]->chronology = nmem_strdup(nmem, "2");
487     h->volumes[0]->enumAndChron = nmem_strdup(nmem, "3");
488
489     h->num_circulationData = 1;
490     h->circulationData = (Z_CircRecord **)
491         nmem_malloc(nmem, 1 * sizeof(Z_CircRecord *));
492     circ = h->circulationData[0] = (Z_CircRecord *)
493         nmem_malloc(nmem, sizeof(Z_CircRecord));
494     circ->availableNow = nmem_booldup(nmem, 1);
495     circ->availablityDate = nmem_strdup(nmem, "20130129");
496     circ->availableThru = 0;
497     circ->restrictions = 0;
498     circ->itemId = nmem_strdup(nmem, "1226176");
499     circ->renewable = nmem_booldup(nmem, 0);
500     circ->onHold = nmem_booldup(nmem, 0);
501     circ->enumAndChron = 0;
502     circ->midspine = 0;
503     circ->temporaryLocation = 0;
504
505     YAZ_CHECK(conv_configure_test("<backend>"
506                                   "<marc"
507                                   " inputcharset=\"marc-8\""
508                                   " outputcharset=\"utf-8\""
509                                   " inputformat=\"marc\""
510                                   " outputformat=\"marcxml\""
511                                   "/>"
512                                   "</backend>",
513                                   0, &p));
514
515     if (p)
516     {
517         WRBUF output_record = wrbuf_alloc();
518         ret = yaz_record_conv_opac_record(p, z_opac, output_record);
519         YAZ_CHECK(ret == 0);
520         if (ret == 0)
521         {
522             ret = strcmp(wrbuf_cstr(output_record), opacxml_rec);
523             YAZ_CHECK(ret == 0);
524             if (ret)
525             {
526                 printf("got-output_record len=%ld: %s\n",
527                        (long) wrbuf_len(output_record),
528                        wrbuf_cstr(output_record));
529                 printf("output_expect_record len=%ld %s\n",
530                        (long) strlen(opacxml_rec),
531                        opacxml_rec);
532             }
533         }
534         yaz_record_conv_destroy(p);
535         wrbuf_destroy(output_record);
536     }
537     {
538         Z_OPACRecord *opac = 0;
539         yaz_marc_t mt =  yaz_marc_create();
540         ret = yaz_xml_to_opac(mt, opacxml_rec, strlen(opacxml_rec),
541                               &opac, 0 /* iconv */, nmem, 0);
542         YAZ_CHECK(ret);
543         YAZ_CHECK(opac);
544
545         if (opac)
546         {
547             WRBUF output_record = wrbuf_alloc();
548             char *p;
549
550             yaz_marc_xml(mt, YAZ_MARC_MARCXML);
551             yaz_opac_decode_wrbuf(mt, opac, output_record);
552
553             /* change MARC size to 00077 from 00078, due to
554                encoding of the aring (two bytes in UTF-8) */
555             p = strstr(wrbuf_buf(output_record), "00078");
556             YAZ_CHECK(p);
557             if (p)
558                 p[4] = '7';
559
560             ret = strcmp(wrbuf_cstr(output_record), opacxml_rec);
561             YAZ_CHECK(ret == 0);
562             if (ret)
563             {
564                 printf("got-output_record len=%ld: %s\n",
565                        (long) wrbuf_len(output_record),
566                        wrbuf_cstr(output_record));
567                 printf("output_expect_record len=%ld %s\n",
568                        (long) strlen(opacxml_rec),
569                        opacxml_rec);
570             }
571             wrbuf_destroy(output_record);
572         }
573         yaz_marc_destroy(mt);
574     }
575     nmem_destroy(nmem);
576 }
577
578 #endif
579
580 int main(int argc, char **argv)
581 {
582     YAZ_CHECK_INIT(argc, argv);
583     yaz_log_xml_errors(0, 0 /* disable log */);
584 #if YAZ_HAVE_XML2
585     tst_configure();
586 #endif
587 #if YAZ_HAVE_XSLT
588     tst_convert1();
589     tst_convert2();
590     tst_convert3();
591     xsltCleanupGlobals();
592 #endif
593 #if YAZ_HAVE_XML2
594     xmlCleanupParser();
595 #endif
596     YAZ_CHECK_TERM;
597 }
598
599 /*
600  * Local variables:
601  * c-basic-offset: 4
602  * c-file-style: "Stroustrup"
603  * indent-tabs-mode: nil
604  * End:
605  * vim: shiftwidth=4 tabstop=8 expandtab
606  */
607