1// Copyright 2018 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package zip
16
17import (
18	"bytes"
19	"encoding/hex"
20	"hash/crc32"
21	"io"
22	"os"
23	"reflect"
24	"syscall"
25	"testing"
26
27	"android/soong/third_party/zip"
28
29	"github.com/google/blueprint/pathtools"
30)
31
32var (
33	fileA        = []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
34	fileB        = []byte("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
35	fileC        = []byte("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")
36	fileEmpty    = []byte("")
37	fileManifest = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\n\n")
38
39	sha256FileA = "d53eda7a637c99cc7fb566d96e9fa109bf15c478410a3f5eb4d4c4e26cd081f6"
40	sha256FileB = "430c56c5818e62bcb6d478901ef86284e97714c138f3c86aa14fd6a84b7ce5d3"
41	sha256FileC = "31c5ab6111f1d6aa13c2c4e92bb3c0f7c76b61b42d141af1e846eb7f6586a51c"
42
43	fileCustomManifest  = []byte("Custom manifest: true\n")
44	customManifestAfter = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\nCustom manifest: true\n\n")
45)
46
47var mockFs = pathtools.MockFs(map[string][]byte{
48	"a/a/a":               fileA,
49	"a/a/b":               fileB,
50	"a/a/c -> ../../c":    nil,
51	"dangling -> missing": nil,
52	"a/a/d -> b":          nil,
53	"c":                   fileC,
54	"d/a/a":               nil,
55	"l_nl":                []byte("a/a/a\na/a/b\nc\n\\[\n"),
56	"l_sp":                []byte("a/a/a a/a/b c \\["),
57	"l2":                  []byte("missing\n"),
58	"rsp":                 []byte("'a/a/a'\na/a/b\n'@'\n'foo'\\''bar'\n'['"),
59	"@ -> c":              nil,
60	"foo'bar -> c":        nil,
61	"manifest.txt":        fileCustomManifest,
62	"[":                   fileEmpty,
63})
64
65func fh(name string, contents []byte, method uint16) zip.FileHeader {
66	return zip.FileHeader{
67		Name:               name,
68		Method:             method,
69		CRC32:              crc32.ChecksumIEEE(contents),
70		UncompressedSize64: uint64(len(contents)),
71		ExternalAttrs:      (syscall.S_IFREG | 0644) << 16,
72	}
73}
74
75func fhWithSHA256(name string, contents []byte, method uint16, sha256 string) zip.FileHeader {
76	h := fh(name, contents, method)
77	// The extra field contains 38 bytes, including 2 bytes of header ID, 2 bytes
78	// of size, 2 bytes of signature, and 32 bytes of checksum data block.
79	var extra [38]byte
80	// The first 6 bytes contains Sha256HeaderID (0x4967), size (unit(34)) and
81	// Sha256HeaderSignature (0x9514)
82	copy(extra[0:], []byte{103, 73, 34, 0, 20, 149})
83	sha256Bytes, _ := hex.DecodeString(sha256)
84	copy(extra[6:], sha256Bytes)
85	h.Extra = append(h.Extra, extra[:]...)
86	return h
87}
88
89func fhManifest(contents []byte) zip.FileHeader {
90	return zip.FileHeader{
91		Name:               "META-INF/MANIFEST.MF",
92		Method:             zip.Store,
93		CRC32:              crc32.ChecksumIEEE(contents),
94		UncompressedSize64: uint64(len(contents)),
95		ExternalAttrs:      (syscall.S_IFREG | 0644) << 16,
96	}
97}
98
99func fhLink(name string, to string) zip.FileHeader {
100	return zip.FileHeader{
101		Name:               name,
102		Method:             zip.Store,
103		CRC32:              crc32.ChecksumIEEE([]byte(to)),
104		UncompressedSize64: uint64(len(to)),
105		ExternalAttrs:      (syscall.S_IFLNK | 0777) << 16,
106	}
107}
108
109type fhDirOptions struct {
110	extra []byte
111}
112
113func fhDir(name string, opts fhDirOptions) zip.FileHeader {
114	return zip.FileHeader{
115		Name:               name,
116		Method:             zip.Store,
117		CRC32:              crc32.ChecksumIEEE(nil),
118		UncompressedSize64: 0,
119		ExternalAttrs:      (syscall.S_IFDIR|0755)<<16 | 0x10,
120		Extra:              opts.extra,
121	}
122}
123
124func fileArgsBuilder() *FileArgsBuilder {
125	return &FileArgsBuilder{
126		fs: mockFs,
127	}
128}
129
130func TestZip(t *testing.T) {
131	testCases := []struct {
132		name               string
133		args               *FileArgsBuilder
134		compressionLevel   int
135		emulateJar         bool
136		nonDeflatedFiles   map[string]bool
137		dirEntries         bool
138		manifest           string
139		storeSymlinks      bool
140		ignoreMissingFiles bool
141		sha256Checksum     bool
142
143		files []zip.FileHeader
144		err   error
145	}{
146		{
147			name: "empty args",
148			args: fileArgsBuilder(),
149
150			files: []zip.FileHeader{},
151		},
152		{
153			name: "files",
154			args: fileArgsBuilder().
155				File("a/a/a").
156				File("a/a/b").
157				File("c").
158				File(`\[`),
159			compressionLevel: 9,
160
161			files: []zip.FileHeader{
162				fh("a/a/a", fileA, zip.Deflate),
163				fh("a/a/b", fileB, zip.Deflate),
164				fh("c", fileC, zip.Deflate),
165				fh("[", fileEmpty, zip.Store),
166			},
167		},
168		{
169			name: "files glob",
170			args: fileArgsBuilder().
171				SourcePrefixToStrip("a").
172				File("a/**/*"),
173			compressionLevel: 9,
174			storeSymlinks:    true,
175
176			files: []zip.FileHeader{
177				fh("a/a", fileA, zip.Deflate),
178				fh("a/b", fileB, zip.Deflate),
179				fhLink("a/c", "../../c"),
180				fhLink("a/d", "b"),
181			},
182		},
183		{
184			name: "dir",
185			args: fileArgsBuilder().
186				SourcePrefixToStrip("a").
187				Dir("a"),
188			compressionLevel: 9,
189			storeSymlinks:    true,
190
191			files: []zip.FileHeader{
192				fh("a/a", fileA, zip.Deflate),
193				fh("a/b", fileB, zip.Deflate),
194				fhLink("a/c", "../../c"),
195				fhLink("a/d", "b"),
196			},
197		},
198		{
199			name: "stored files",
200			args: fileArgsBuilder().
201				File("a/a/a").
202				File("a/a/b").
203				File("c"),
204			compressionLevel: 0,
205
206			files: []zip.FileHeader{
207				fh("a/a/a", fileA, zip.Store),
208				fh("a/a/b", fileB, zip.Store),
209				fh("c", fileC, zip.Store),
210			},
211		},
212		{
213			name: "symlinks in zip",
214			args: fileArgsBuilder().
215				File("a/a/a").
216				File("a/a/b").
217				File("a/a/c").
218				File("a/a/d"),
219			compressionLevel: 9,
220			storeSymlinks:    true,
221
222			files: []zip.FileHeader{
223				fh("a/a/a", fileA, zip.Deflate),
224				fh("a/a/b", fileB, zip.Deflate),
225				fhLink("a/a/c", "../../c"),
226				fhLink("a/a/d", "b"),
227			},
228		},
229		{
230			name: "follow symlinks",
231			args: fileArgsBuilder().
232				File("a/a/a").
233				File("a/a/b").
234				File("a/a/c").
235				File("a/a/d"),
236			compressionLevel: 9,
237			storeSymlinks:    false,
238
239			files: []zip.FileHeader{
240				fh("a/a/a", fileA, zip.Deflate),
241				fh("a/a/b", fileB, zip.Deflate),
242				fh("a/a/c", fileC, zip.Deflate),
243				fh("a/a/d", fileB, zip.Deflate),
244			},
245		},
246		{
247			name: "dangling symlinks",
248			args: fileArgsBuilder().
249				File("dangling"),
250			compressionLevel: 9,
251			storeSymlinks:    true,
252
253			files: []zip.FileHeader{
254				fhLink("dangling", "missing"),
255			},
256		},
257		{
258			name: "list",
259			args: fileArgsBuilder().
260				List("l_nl"),
261			compressionLevel: 9,
262
263			files: []zip.FileHeader{
264				fh("a/a/a", fileA, zip.Deflate),
265				fh("a/a/b", fileB, zip.Deflate),
266				fh("c", fileC, zip.Deflate),
267				fh("[", fileEmpty, zip.Store),
268			},
269		},
270		{
271			name: "list",
272			args: fileArgsBuilder().
273				List("l_sp"),
274			compressionLevel: 9,
275
276			files: []zip.FileHeader{
277				fh("a/a/a", fileA, zip.Deflate),
278				fh("a/a/b", fileB, zip.Deflate),
279				fh("c", fileC, zip.Deflate),
280				fh("[", fileEmpty, zip.Store),
281			},
282		},
283		{
284			name: "rsp",
285			args: fileArgsBuilder().
286				RspFile("rsp"),
287			compressionLevel: 9,
288
289			files: []zip.FileHeader{
290				fh("a/a/a", fileA, zip.Deflate),
291				fh("a/a/b", fileB, zip.Deflate),
292				fh("@", fileC, zip.Deflate),
293				fh("foo'bar", fileC, zip.Deflate),
294				fh("[", fileEmpty, zip.Store),
295			},
296		},
297		{
298			name: "prefix in zip",
299			args: fileArgsBuilder().
300				PathPrefixInZip("foo").
301				File("a/a/a").
302				File("a/a/b").
303				File("c"),
304			compressionLevel: 9,
305
306			files: []zip.FileHeader{
307				fh("foo/a/a/a", fileA, zip.Deflate),
308				fh("foo/a/a/b", fileB, zip.Deflate),
309				fh("foo/c", fileC, zip.Deflate),
310			},
311		},
312		{
313			name: "relative root",
314			args: fileArgsBuilder().
315				SourcePrefixToStrip("a").
316				File("a/a/a").
317				File("a/a/b"),
318			compressionLevel: 9,
319
320			files: []zip.FileHeader{
321				fh("a/a", fileA, zip.Deflate),
322				fh("a/b", fileB, zip.Deflate),
323			},
324		},
325		{
326			name: "multiple relative root",
327			args: fileArgsBuilder().
328				SourcePrefixToStrip("a").
329				File("a/a/a").
330				SourcePrefixToStrip("a/a").
331				File("a/a/b"),
332			compressionLevel: 9,
333
334			files: []zip.FileHeader{
335				fh("a/a", fileA, zip.Deflate),
336				fh("b", fileB, zip.Deflate),
337			},
338		},
339		{
340			name: "emulate jar",
341			args: fileArgsBuilder().
342				File("a/a/a").
343				File("a/a/b"),
344			compressionLevel: 9,
345			emulateJar:       true,
346
347			files: []zip.FileHeader{
348				fhDir("META-INF/", fhDirOptions{extra: []byte{254, 202, 0, 0}}),
349				fhManifest(fileManifest),
350				fhDir("a/", fhDirOptions{}),
351				fhDir("a/a/", fhDirOptions{}),
352				fh("a/a/a", fileA, zip.Deflate),
353				fh("a/a/b", fileB, zip.Deflate),
354			},
355		},
356		{
357			name: "emulate jar with manifest",
358			args: fileArgsBuilder().
359				File("a/a/a").
360				File("a/a/b"),
361			compressionLevel: 9,
362			emulateJar:       true,
363			manifest:         "manifest.txt",
364
365			files: []zip.FileHeader{
366				fhDir("META-INF/", fhDirOptions{extra: []byte{254, 202, 0, 0}}),
367				fhManifest(customManifestAfter),
368				fhDir("a/", fhDirOptions{}),
369				fhDir("a/a/", fhDirOptions{}),
370				fh("a/a/a", fileA, zip.Deflate),
371				fh("a/a/b", fileB, zip.Deflate),
372			},
373		},
374		{
375			name: "dir entries",
376			args: fileArgsBuilder().
377				File("a/a/a").
378				File("a/a/b"),
379			compressionLevel: 9,
380			dirEntries:       true,
381
382			files: []zip.FileHeader{
383				fhDir("a/", fhDirOptions{}),
384				fhDir("a/a/", fhDirOptions{}),
385				fh("a/a/a", fileA, zip.Deflate),
386				fh("a/a/b", fileB, zip.Deflate),
387			},
388		},
389		{
390			name: "junk paths",
391			args: fileArgsBuilder().
392				JunkPaths(true).
393				File("a/a/a").
394				File("a/a/b"),
395			compressionLevel: 9,
396
397			files: []zip.FileHeader{
398				fh("a", fileA, zip.Deflate),
399				fh("b", fileB, zip.Deflate),
400			},
401		},
402		{
403			name: "non deflated files",
404			args: fileArgsBuilder().
405				File("a/a/a").
406				File("a/a/b"),
407			compressionLevel: 9,
408			nonDeflatedFiles: map[string]bool{"a/a/a": true},
409
410			files: []zip.FileHeader{
411				fh("a/a/a", fileA, zip.Store),
412				fh("a/a/b", fileB, zip.Deflate),
413			},
414		},
415		{
416			name: "ignore missing files",
417			args: fileArgsBuilder().
418				File("a/a/a").
419				File("a/a/b").
420				File("missing"),
421			compressionLevel:   9,
422			ignoreMissingFiles: true,
423
424			files: []zip.FileHeader{
425				fh("a/a/a", fileA, zip.Deflate),
426				fh("a/a/b", fileB, zip.Deflate),
427			},
428		},
429		{
430			name: "duplicate sources",
431			args: fileArgsBuilder().
432				File("a/a/a").
433				File("a/a/a"),
434			compressionLevel: 9,
435
436			files: []zip.FileHeader{
437				fh("a/a/a", fileA, zip.Deflate),
438			},
439		},
440		{
441			name: "generate SHA256 checksum",
442			args: fileArgsBuilder().
443				File("a/a/a").
444				File("a/a/b").
445				File("a/a/c").
446				File("c"),
447			compressionLevel: 9,
448			sha256Checksum:   true,
449
450			files: []zip.FileHeader{
451				fhWithSHA256("a/a/a", fileA, zip.Deflate, sha256FileA),
452				fhWithSHA256("a/a/b", fileB, zip.Deflate, sha256FileB),
453				fhWithSHA256("a/a/c", fileC, zip.Deflate, sha256FileC),
454				fhWithSHA256("c", fileC, zip.Deflate, sha256FileC),
455			},
456		},
457		{
458			name: "explicit path",
459			args: fileArgsBuilder().
460				ExplicitPathInZip("foo").
461				File("a/a/a").
462				File("a/a/b"),
463			compressionLevel: 9,
464
465			files: []zip.FileHeader{
466				fh("foo", fileA, zip.Deflate),
467				fh("a/a/b", fileB, zip.Deflate),
468			},
469		},
470		{
471			name: "explicit path with prefix",
472			args: fileArgsBuilder().
473				PathPrefixInZip("prefix").
474				ExplicitPathInZip("foo").
475				File("a/a/a").
476				File("a/a/b"),
477			compressionLevel: 9,
478
479			files: []zip.FileHeader{
480				fh("prefix/foo", fileA, zip.Deflate),
481				fh("prefix/a/a/b", fileB, zip.Deflate),
482			},
483		},
484		{
485			name: "explicit path with glob",
486			args: fileArgsBuilder().
487				ExplicitPathInZip("foo").
488				File("a/a/a*").
489				File("a/a/b"),
490			compressionLevel: 9,
491
492			files: []zip.FileHeader{
493				fh("foo", fileA, zip.Deflate),
494				fh("a/a/b", fileB, zip.Deflate),
495			},
496		},
497		{
498			name: "explicit path with junk paths",
499			args: fileArgsBuilder().
500				JunkPaths(true).
501				ExplicitPathInZip("foo/bar").
502				File("a/a/a*").
503				File("a/a/b"),
504			compressionLevel: 9,
505
506			files: []zip.FileHeader{
507				fh("foo/bar", fileA, zip.Deflate),
508				fh("b", fileB, zip.Deflate),
509			},
510		},
511
512		// errors
513		{
514			name: "error missing file",
515			args: fileArgsBuilder().
516				File("missing"),
517			err: os.ErrNotExist,
518		},
519		{
520			name: "error missing dir",
521			args: fileArgsBuilder().
522				Dir("missing"),
523			err: os.ErrNotExist,
524		},
525		{
526			name: "error missing file in list",
527			args: fileArgsBuilder().
528				List("l2"),
529			err: os.ErrNotExist,
530		},
531		{
532			name: "error incorrect relative root",
533			args: fileArgsBuilder().
534				SourcePrefixToStrip("b").
535				File("a/a/a"),
536			err: IncorrectRelativeRootError{},
537		},
538		{
539			name: "error conflicting file",
540			args: fileArgsBuilder().
541				SourcePrefixToStrip("a").
542				File("a/a/a").
543				SourcePrefixToStrip("d").
544				File("d/a/a"),
545			err: ConflictingFileError{},
546		},
547		{
548			name: "error explicit path conflicting",
549			args: fileArgsBuilder().
550				ExplicitPathInZip("foo").
551				File("a/a/a").
552				ExplicitPathInZip("foo").
553				File("a/a/b"),
554			err: ConflictingFileError{},
555		},
556		{
557			name: "error explicit path conflicting glob",
558			args: fileArgsBuilder().
559				ExplicitPathInZip("foo").
560				File("a/a/*"),
561			err: ConflictingFileError{},
562		},
563	}
564
565	for _, test := range testCases {
566		t.Run(test.name, func(t *testing.T) {
567			if test.args.Error() != nil {
568				t.Fatal(test.args.Error())
569			}
570
571			args := ZipArgs{}
572			args.FileArgs = test.args.FileArgs()
573			args.CompressionLevel = test.compressionLevel
574			args.EmulateJar = test.emulateJar
575			args.AddDirectoryEntriesToZip = test.dirEntries
576			args.NonDeflatedFiles = test.nonDeflatedFiles
577			args.ManifestSourcePath = test.manifest
578			args.StoreSymlinks = test.storeSymlinks
579			args.IgnoreMissingFiles = test.ignoreMissingFiles
580			args.Sha256Checksum = test.sha256Checksum
581			args.Filesystem = mockFs
582			args.Stderr = &bytes.Buffer{}
583
584			buf := &bytes.Buffer{}
585			err := zipTo(args, buf)
586
587			if (err != nil) != (test.err != nil) {
588				t.Fatalf("want error %v, got %v", test.err, err)
589			} else if test.err != nil {
590				if os.IsNotExist(test.err) {
591					if !os.IsNotExist(err) {
592						t.Fatalf("want error %v, got %v", test.err, err)
593					}
594				} else if _, wantRelativeRootErr := test.err.(IncorrectRelativeRootError); wantRelativeRootErr {
595					if _, gotRelativeRootErr := err.(IncorrectRelativeRootError); !gotRelativeRootErr {
596						t.Fatalf("want error %v, got %v", test.err, err)
597					}
598				} else if _, wantConflictingFileError := test.err.(ConflictingFileError); wantConflictingFileError {
599					if _, gotConflictingFileError := err.(ConflictingFileError); !gotConflictingFileError {
600						t.Fatalf("want error %v, got %v", test.err, err)
601					}
602				} else {
603					t.Fatalf("want error %v, got %v", test.err, err)
604				}
605				return
606			}
607
608			br := bytes.NewReader(buf.Bytes())
609			zr, err := zip.NewReader(br, int64(br.Len()))
610			if err != nil {
611				t.Fatal(err)
612			}
613
614			var files []zip.FileHeader
615			for _, f := range zr.File {
616				r, err := f.Open()
617				if err != nil {
618					t.Fatalf("error when opening %s: %s", f.Name, err)
619				}
620
621				crc := crc32.NewIEEE()
622				len, err := io.Copy(crc, r)
623				r.Close()
624				if err != nil {
625					t.Fatalf("error when reading %s: %s", f.Name, err)
626				}
627
628				if uint64(len) != f.UncompressedSize64 {
629					t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
630				}
631
632				if crc.Sum32() != f.CRC32 {
633					t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
634				}
635
636				files = append(files, f.FileHeader)
637			}
638
639			if len(files) != len(test.files) {
640				t.Fatalf("want %d files, got %d", len(test.files), len(files))
641			}
642
643			for i := range files {
644				want := test.files[i]
645				got := files[i]
646
647				if want.Name != got.Name {
648					t.Errorf("incorrect file %d want %q got %q", i, want.Name, got.Name)
649					continue
650				}
651
652				if want.UncompressedSize64 != got.UncompressedSize64 {
653					t.Errorf("incorrect file %s length want %v got %v", want.Name,
654						want.UncompressedSize64, got.UncompressedSize64)
655				}
656
657				if want.ExternalAttrs != got.ExternalAttrs {
658					t.Errorf("incorrect file %s attrs want %x got %x", want.Name,
659						want.ExternalAttrs, got.ExternalAttrs)
660				}
661
662				if want.CRC32 != got.CRC32 {
663					t.Errorf("incorrect file %s crc want %v got %v", want.Name,
664						want.CRC32, got.CRC32)
665				}
666
667				if want.Method != got.Method {
668					t.Errorf("incorrect file %s method want %v got %v", want.Name,
669						want.Method, got.Method)
670				}
671
672				if !bytes.Equal(want.Extra, got.Extra) {
673					t.Errorf("incorrect file %s extra want %v got %v", want.Name,
674						want.Extra, got.Extra)
675				}
676			}
677		})
678	}
679}
680
681func TestSrcJar(t *testing.T) {
682	mockFs := pathtools.MockFs(map[string][]byte{
683		"wrong_package.java":       []byte("package foo;"),
684		"foo/correct_package.java": []byte("package foo;"),
685		"src/no_package.java":      nil,
686		"src2/parse_error.java":    []byte("error"),
687	})
688
689	want := []string{
690		"foo/",
691		"foo/wrong_package.java",
692		"foo/correct_package.java",
693		"no_package.java",
694		"src2/",
695		"src2/parse_error.java",
696	}
697
698	args := ZipArgs{}
699	args.FileArgs = NewFileArgsBuilder().File("**/*.java").FileArgs()
700
701	args.SrcJar = true
702	args.AddDirectoryEntriesToZip = true
703	args.Filesystem = mockFs
704	args.Stderr = &bytes.Buffer{}
705
706	buf := &bytes.Buffer{}
707	err := zipTo(args, buf)
708	if err != nil {
709		t.Fatalf("got error %v", err)
710	}
711
712	br := bytes.NewReader(buf.Bytes())
713	zr, err := zip.NewReader(br, int64(br.Len()))
714	if err != nil {
715		t.Fatal(err)
716	}
717
718	var got []string
719	for _, f := range zr.File {
720		r, err := f.Open()
721		if err != nil {
722			t.Fatalf("error when opening %s: %s", f.Name, err)
723		}
724
725		crc := crc32.NewIEEE()
726		len, err := io.Copy(crc, r)
727		r.Close()
728		if err != nil {
729			t.Fatalf("error when reading %s: %s", f.Name, err)
730		}
731
732		if uint64(len) != f.UncompressedSize64 {
733			t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
734		}
735
736		if crc.Sum32() != f.CRC32 {
737			t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
738		}
739
740		got = append(got, f.Name)
741	}
742
743	if !reflect.DeepEqual(want, got) {
744		t.Errorf("want files %q, got %q", want, got)
745	}
746}
747