Source file
src/path/filepath/path_windows_test.go
1
2
3
4
5 package filepath_test
6
7 import (
8 "flag"
9 "fmt"
10 "internal/godebug"
11 "internal/testenv"
12 "io/fs"
13 "os"
14 "os/exec"
15 "path/filepath"
16 "runtime/debug"
17 "slices"
18 "strings"
19 "testing"
20 )
21
22 func TestWinSplitListTestsAreValid(t *testing.T) {
23 comspec := os.Getenv("ComSpec")
24 if comspec == "" {
25 t.Fatal("%ComSpec% must be set")
26 }
27
28 for ti, tt := range winsplitlisttests {
29 testWinSplitListTestIsValid(t, ti, tt, comspec)
30 }
31 }
32
33 func testWinSplitListTestIsValid(t *testing.T, ti int, tt SplitListTest,
34 comspec string) {
35
36 const (
37 cmdfile = `printdir.cmd`
38 perm fs.FileMode = 0700
39 )
40
41 tmp := t.TempDir()
42 for i, d := range tt.result {
43 if d == "" {
44 continue
45 }
46 if cd := filepath.Clean(d); filepath.VolumeName(cd) != "" ||
47 cd[0] == '\\' || cd == ".." || (len(cd) >= 3 && cd[0:3] == `..\`) {
48 t.Errorf("%d,%d: %#q refers outside working directory", ti, i, d)
49 return
50 }
51 dd := filepath.Join(tmp, d)
52 if _, err := os.Stat(dd); err == nil {
53 t.Errorf("%d,%d: %#q already exists", ti, i, d)
54 return
55 }
56 if err := os.MkdirAll(dd, perm); err != nil {
57 t.Errorf("%d,%d: MkdirAll(%#q) failed: %v", ti, i, dd, err)
58 return
59 }
60 fn, data := filepath.Join(dd, cmdfile), []byte("@echo "+d+"\r\n")
61 if err := os.WriteFile(fn, data, perm); err != nil {
62 t.Errorf("%d,%d: WriteFile(%#q) failed: %v", ti, i, fn, err)
63 return
64 }
65 }
66
67
68 systemRoot := os.Getenv("SystemRoot")
69
70 for i, d := range tt.result {
71 if d == "" {
72 continue
73 }
74 exp := []byte(d + "\r\n")
75 cmd := &exec.Cmd{
76 Path: comspec,
77 Args: []string{`/c`, cmdfile},
78 Env: []string{`Path=` + systemRoot + "/System32;" + tt.list, `SystemRoot=` + systemRoot},
79 Dir: tmp,
80 }
81 out, err := cmd.CombinedOutput()
82 switch {
83 case err != nil:
84 t.Errorf("%d,%d: execution error %v\n%q", ti, i, err, out)
85 return
86 case !slices.Equal(out, exp):
87 t.Errorf("%d,%d: expected %#q, got %#q", ti, i, exp, out)
88 return
89 default:
90
91 err = os.Remove(filepath.Join(tmp, d, cmdfile))
92 if err != nil {
93 t.Fatalf("Remove test command failed: %v", err)
94 }
95 }
96 }
97 }
98
99 func TestWindowsEvalSymlinks(t *testing.T) {
100 testenv.MustHaveSymlink(t)
101
102 tmpDir := tempDirCanonical(t)
103
104 if len(tmpDir) < 3 {
105 t.Fatalf("tmpDir path %q is too short", tmpDir)
106 }
107 if tmpDir[1] != ':' {
108 t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir)
109 }
110 test := EvalSymlinksTest{"test/linkabswin", tmpDir[:3]}
111
112
113 testdirs := append(EvalSymlinksTestDirs, test)
114 for _, d := range testdirs {
115 var err error
116 path := simpleJoin(tmpDir, d.path)
117 if d.dest == "" {
118 err = os.Mkdir(path, 0755)
119 } else {
120 err = os.Symlink(d.dest, path)
121 }
122 if err != nil {
123 t.Fatal(err)
124 }
125 }
126
127 path := simpleJoin(tmpDir, test.path)
128
129 testEvalSymlinks(t, path, test.dest)
130
131 testEvalSymlinksAfterChdir(t, path, ".", test.dest)
132
133 testEvalSymlinksAfterChdir(t,
134 path,
135 filepath.VolumeName(tmpDir)+".",
136 test.dest)
137
138 testEvalSymlinksAfterChdir(t,
139 simpleJoin(tmpDir, "test"),
140 simpleJoin("..", test.path),
141 test.dest)
142
143 testEvalSymlinksAfterChdir(t, tmpDir, test.path, test.dest)
144 }
145
146
147
148 func TestEvalSymlinksCanonicalNames(t *testing.T) {
149 ctmp := tempDirCanonical(t)
150 dirs := []string{
151 "test",
152 "test/dir",
153 "testing_long_dir",
154 "TEST2",
155 }
156
157 for _, d := range dirs {
158 dir := filepath.Join(ctmp, d)
159 err := os.Mkdir(dir, 0755)
160 if err != nil {
161 t.Fatal(err)
162 }
163 cname, err := filepath.EvalSymlinks(dir)
164 if err != nil {
165 t.Errorf("EvalSymlinks(%q) error: %v", dir, err)
166 continue
167 }
168 if dir != cname {
169 t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", dir, cname, dir)
170 continue
171 }
172
173 test := strings.ToUpper(dir)
174 p, err := filepath.EvalSymlinks(test)
175 if err != nil {
176 t.Errorf("EvalSymlinks(%q) error: %v", test, err)
177 continue
178 }
179 if p != cname {
180 t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname)
181 continue
182 }
183
184 test = strings.ToLower(dir)
185 p, err = filepath.EvalSymlinks(test)
186 if err != nil {
187 t.Errorf("EvalSymlinks(%q) error: %v", test, err)
188 continue
189 }
190 if p != cname {
191 t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname)
192 continue
193 }
194 }
195 }
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215 func checkVolume8dot3Setting(vol string, enabled bool) error {
216
217
218 out, _ := exec.Command("fsutil", "8dot3name", "query", vol).CombinedOutput()
219
220 expected := "The registry state of NtfsDisable8dot3NameCreation is 2, the default (Volume level setting)"
221 if !strings.Contains(string(out), expected) {
222
223 expectedWindow10 := "The registry state is: 2 (Per volume setting - the default)"
224 if !strings.Contains(string(out), expectedWindow10) {
225 return fmt.Errorf("fsutil output should contain %q, but is %q", expected, string(out))
226 }
227 }
228
229 expected = "Based on the above two settings, 8dot3 name creation is %s on %s"
230 if enabled {
231 expected = fmt.Sprintf(expected, "enabled", vol)
232 } else {
233 expected = fmt.Sprintf(expected, "disabled", vol)
234 }
235 if !strings.Contains(string(out), expected) {
236 return fmt.Errorf("unexpected fsutil output: %q", string(out))
237 }
238 return nil
239 }
240
241 func setVolume8dot3Setting(vol string, enabled bool) error {
242 cmd := []string{"fsutil", "8dot3name", "set", vol}
243 if enabled {
244 cmd = append(cmd, "0")
245 } else {
246 cmd = append(cmd, "1")
247 }
248
249
250 out, _ := exec.Command(cmd[0], cmd[1:]...).CombinedOutput()
251 if string(out) != "\r\nSuccessfully set 8dot3name behavior.\r\n" {
252
253 expectedWindow10 := "Successfully %s 8dot3name generation on %s\r\n"
254 if enabled {
255 expectedWindow10 = fmt.Sprintf(expectedWindow10, "enabled", vol)
256 } else {
257 expectedWindow10 = fmt.Sprintf(expectedWindow10, "disabled", vol)
258 }
259 if string(out) != expectedWindow10 {
260 return fmt.Errorf("%v command failed: %q", cmd, string(out))
261 }
262 }
263 return nil
264 }
265
266 var runFSModifyTests = flag.Bool("run_fs_modify_tests", false, "run tests which modify filesystem parameters")
267
268
269
270 func TestEvalSymlinksCanonicalNamesWith8dot3Disabled(t *testing.T) {
271 if !*runFSModifyTests {
272 t.Skip("skipping test that modifies file system setting; enable with -run_fs_modify_tests")
273 }
274 tempVol := filepath.VolumeName(os.TempDir())
275 if len(tempVol) != 2 {
276 t.Fatalf("unexpected temp volume name %q", tempVol)
277 }
278
279 err := checkVolume8dot3Setting(tempVol, true)
280 if err != nil {
281 t.Fatal(err)
282 }
283 err = setVolume8dot3Setting(tempVol, false)
284 if err != nil {
285 t.Fatal(err)
286 }
287 defer func() {
288 err := setVolume8dot3Setting(tempVol, true)
289 if err != nil {
290 t.Fatal(err)
291 }
292 err = checkVolume8dot3Setting(tempVol, true)
293 if err != nil {
294 t.Fatal(err)
295 }
296 }()
297 err = checkVolume8dot3Setting(tempVol, false)
298 if err != nil {
299 t.Fatal(err)
300 }
301 TestEvalSymlinksCanonicalNames(t)
302 }
303
304 func TestToNorm(t *testing.T) {
305 stubBase := func(path string) (string, error) {
306 vol := filepath.VolumeName(path)
307 path = path[len(vol):]
308
309 if strings.Contains(path, "/") {
310 return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
311 }
312
313 if path == "" || path == "." || path == `\` {
314 return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
315 }
316
317 i := strings.LastIndexByte(path, filepath.Separator)
318 if i == len(path)-1 {
319 return "", fmt.Errorf("invalid path is given to base: %s", vol+path)
320 }
321 if i == -1 {
322 return strings.ToUpper(path), nil
323 }
324
325 return strings.ToUpper(path[i+1:]), nil
326 }
327
328
329 tests := []struct {
330 arg string
331 want string
332 }{
333 {"", ""},
334 {".", "."},
335 {"./foo/bar", `FOO\BAR`},
336 {"/", `\`},
337 {"/foo/bar", `\FOO\BAR`},
338 {"/foo/bar/baz/qux", `\FOO\BAR\BAZ\QUX`},
339 {"foo/bar", `FOO\BAR`},
340 {"C:/foo/bar", `C:\FOO\BAR`},
341 {"C:foo/bar", `C:FOO\BAR`},
342 {"c:/foo/bar", `C:\FOO\BAR`},
343 {"C:/foo/bar", `C:\FOO\BAR`},
344 {"C:/foo/bar/", `C:\FOO\BAR`},
345 {`C:\foo\bar`, `C:\FOO\BAR`},
346 {`C:\foo/bar\`, `C:\FOO\BAR`},
347 {"C:/ふー/バー", `C:\ふー\バー`},
348 }
349
350 for _, test := range tests {
351 var path string
352 if test.arg != "" {
353 path = filepath.Clean(test.arg)
354 }
355 got, err := filepath.ToNorm(path, stubBase)
356 if err != nil {
357 t.Errorf("toNorm(%s) failed: %v\n", test.arg, err)
358 } else if got != test.want {
359 t.Errorf("toNorm(%s) returns %s, but %s expected\n", test.arg, got, test.want)
360 }
361 }
362
363 testPath := `{{tmp}}\test\foo\bar`
364
365 testsDir := []struct {
366 wd string
367 arg string
368 want string
369 }{
370
371 {".", `{{tmp}}\test\foo\bar`, `{{tmp}}\test\foo\bar`},
372 {".", `{{tmp}}\.\test/foo\bar`, `{{tmp}}\test\foo\bar`},
373 {".", `{{tmp}}\test\..\test\foo\bar`, `{{tmp}}\test\foo\bar`},
374 {".", `{{tmp}}\TEST\FOO\BAR`, `{{tmp}}\test\foo\bar`},
375
376
377 {`{{tmp}}\test`, `{{tmpvol}}.`, `{{tmpvol}}.`},
378 {`{{tmp}}\test`, `{{tmpvol}}..`, `{{tmpvol}}..`},
379 {`{{tmp}}\test`, `{{tmpvol}}foo\bar`, `{{tmpvol}}foo\bar`},
380 {`{{tmp}}\test`, `{{tmpvol}}.\foo\bar`, `{{tmpvol}}foo\bar`},
381 {`{{tmp}}\test`, `{{tmpvol}}foo\..\foo\bar`, `{{tmpvol}}foo\bar`},
382 {`{{tmp}}\test`, `{{tmpvol}}FOO\BAR`, `{{tmpvol}}foo\bar`},
383
384
385 {"{{tmp}}", `{{tmpnovol}}\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
386 {"{{tmp}}", `{{tmpnovol}}\.\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
387 {"{{tmp}}", `{{tmpnovol}}\test\..\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`},
388 {"{{tmp}}", `{{tmpnovol}}\TEST\FOO\BAR`, `{{tmpnovol}}\test\foo\bar`},
389
390
391 {`{{tmp}}\test`, ".", `.`},
392 {`{{tmp}}\test`, "..", `..`},
393 {`{{tmp}}\test`, `foo\bar`, `foo\bar`},
394 {`{{tmp}}\test`, `.\foo\bar`, `foo\bar`},
395 {`{{tmp}}\test`, `foo\..\foo\bar`, `foo\bar`},
396 {`{{tmp}}\test`, `FOO\BAR`, `foo\bar`},
397
398
399 {".", `\\localhost\c$`, `\\localhost\c$`},
400 }
401
402 ctmp := tempDirCanonical(t)
403 if err := os.MkdirAll(strings.ReplaceAll(testPath, "{{tmp}}", ctmp), 0777); err != nil {
404 t.Fatal(err)
405 }
406
407 cwd, err := os.Getwd()
408 if err != nil {
409 t.Fatal(err)
410 }
411 t.Chdir(".")
412
413 tmpVol := filepath.VolumeName(ctmp)
414 if len(tmpVol) != 2 {
415 t.Fatalf("unexpected temp volume name %q", tmpVol)
416 }
417
418 tmpNoVol := ctmp[len(tmpVol):]
419
420 replacer := strings.NewReplacer("{{tmp}}", ctmp, "{{tmpvol}}", tmpVol, "{{tmpnovol}}", tmpNoVol)
421
422 for _, test := range testsDir {
423 wd := replacer.Replace(test.wd)
424 arg := replacer.Replace(test.arg)
425 want := replacer.Replace(test.want)
426
427 if test.wd == "." {
428 err := os.Chdir(cwd)
429 if err != nil {
430 t.Error(err)
431
432 continue
433 }
434 } else {
435 err := os.Chdir(wd)
436 if err != nil {
437 t.Error(err)
438
439 continue
440 }
441 }
442 if arg != "" {
443 arg = filepath.Clean(arg)
444 }
445 got, err := filepath.ToNorm(arg, filepath.NormBase)
446 if err != nil {
447 t.Errorf("toNorm(%s) failed: %v (wd=%s)\n", arg, err, wd)
448 } else if got != want {
449 t.Errorf("toNorm(%s) returns %s, but %s expected (wd=%s)\n", arg, got, want, wd)
450 }
451 }
452 }
453
454 func TestUNC(t *testing.T) {
455
456
457 defer debug.SetMaxStack(debug.SetMaxStack(1e6))
458 filepath.Glob(`\\?\c:\*`)
459 }
460
461 func testWalkMklink(t *testing.T, linktype string) {
462 output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
463 if !strings.Contains(string(output), fmt.Sprintf(" /%s ", linktype)) {
464 t.Skipf(`skipping test; mklink does not supports /%s parameter`, linktype)
465 }
466 testWalkSymlink(t, func(target, link string) error {
467 output, err := exec.Command("cmd", "/c", "mklink", "/"+linktype, link, target).CombinedOutput()
468 if err != nil {
469 return fmt.Errorf(`"mklink /%s %v %v" command failed: %v\n%v`, linktype, link, target, err, string(output))
470 }
471 return nil
472 })
473 }
474
475 func TestWalkDirectoryJunction(t *testing.T) {
476 testenv.MustHaveSymlink(t)
477 testWalkMklink(t, "J")
478 }
479
480 func TestWalkDirectorySymlink(t *testing.T) {
481 testenv.MustHaveSymlink(t)
482 testWalkMklink(t, "D")
483 }
484
485 func createMountPartition(t *testing.T, vhd string, args string) []byte {
486 testenv.MustHaveExecPath(t, "powershell")
487 t.Cleanup(func() {
488 cmd := testenv.Command(t, "powershell", "-Command", fmt.Sprintf("Dismount-VHD %q", vhd))
489 out, err := cmd.CombinedOutput()
490 if err != nil {
491 if t.Skipped() {
492
493
494 t.Logf("%v: %v (skipped)\n%s", cmd, err, out)
495 } else {
496
497
498 t.Errorf("%v: %v\n%s", cmd, err, out)
499 }
500 }
501 })
502
503 script := filepath.Join(t.TempDir(), "test.ps1")
504 cmd := strings.Join([]string{
505 "$ErrorActionPreference = \"Stop\"",
506 fmt.Sprintf("$vhd = New-VHD -Path %q -SizeBytes 3MB -Fixed", vhd),
507 "$vhd | Mount-VHD",
508 fmt.Sprintf("$vhd = Get-VHD %q", vhd),
509 "$vhd | Get-Disk | Initialize-Disk -PartitionStyle GPT",
510 "$part = $vhd | Get-Disk | New-Partition -UseMaximumSize -AssignDriveLetter:$false",
511 "$vol = $part | Format-Volume -FileSystem NTFS",
512 args,
513 }, "\n")
514
515 err := os.WriteFile(script, []byte(cmd), 0666)
516 if err != nil {
517 t.Fatal(err)
518 }
519 output, err := testenv.Command(t, "powershell", "-File", script).CombinedOutput()
520 if err != nil {
521
522 t.Skip("skipping test because failed to create VHD: ", err, string(output))
523 }
524 return output
525 }
526
527 var winsymlink = godebug.New("winsymlink")
528 var winreadlinkvolume = godebug.New("winreadlinkvolume")
529
530 func TestEvalSymlinksJunctionToVolumeID(t *testing.T) {
531
532
533 if winsymlink.Value() == "0" {
534 t.Skip("skipping test because winsymlink is not enabled")
535 }
536 t.Parallel()
537
538 output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
539 if !strings.Contains(string(output), " /J ") {
540 t.Skip("skipping test because mklink command does not support junctions")
541 }
542
543 tmpdir := tempDirCanonical(t)
544 vhd := filepath.Join(tmpdir, "Test.vhdx")
545 output = createMountPartition(t, vhd, "Write-Host $vol.Path -NoNewline")
546 vol := string(output)
547
548 dirlink := filepath.Join(tmpdir, "dirlink")
549 output, err := testenv.Command(t, "cmd", "/c", "mklink", "/J", dirlink, vol).CombinedOutput()
550 if err != nil {
551 t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, vol, err, output)
552 }
553 got, err := filepath.EvalSymlinks(dirlink)
554 if err != nil {
555 t.Fatal(err)
556 }
557 if got != dirlink {
558 t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, dirlink)
559 }
560 }
561
562 func TestEvalSymlinksMountPointRecursion(t *testing.T) {
563
564
565 if winsymlink.Value() == "0" {
566 t.Skip("skipping test because winsymlink is not enabled")
567 }
568 t.Parallel()
569
570 tmpdir := tempDirCanonical(t)
571 dirlink := filepath.Join(tmpdir, "dirlink")
572 err := os.Mkdir(dirlink, 0755)
573 if err != nil {
574 t.Fatal(err)
575 }
576
577 vhd := filepath.Join(tmpdir, "Test.vhdx")
578 createMountPartition(t, vhd, fmt.Sprintf("$part | Add-PartitionAccessPath -AccessPath %q\n", dirlink))
579
580 got, err := filepath.EvalSymlinks(dirlink)
581 if err != nil {
582 t.Fatal(err)
583 }
584 if got != dirlink {
585 t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, dirlink)
586 }
587 }
588
589 func TestNTNamespaceSymlink(t *testing.T) {
590 output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output()
591 if !strings.Contains(string(output), " /J ") {
592 t.Skip("skipping test because mklink command does not support junctions")
593 }
594
595 tmpdir := tempDirCanonical(t)
596
597 vol := filepath.VolumeName(tmpdir)
598 output, err := exec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput()
599 if err != nil {
600 t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output)
601 }
602 target := strings.Trim(string(output), " \n\r")
603
604 dirlink := filepath.Join(tmpdir, "dirlink")
605 output, err = exec.Command("cmd", "/c", "mklink", "/J", dirlink, target).CombinedOutput()
606 if err != nil {
607 t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, target, err, output)
608 }
609
610 got, err := filepath.EvalSymlinks(dirlink)
611 if err != nil {
612 t.Fatal(err)
613 }
614 var want string
615 if winsymlink.Value() == "0" {
616 if winreadlinkvolume.Value() == "0" {
617 want = vol + `\`
618 } else {
619 want = target
620 }
621 } else {
622 want = dirlink
623 }
624 if got != want {
625 t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, want)
626 }
627
628
629 testenv.MustHaveSymlink(t)
630
631 file := filepath.Join(tmpdir, "file")
632 err = os.WriteFile(file, []byte(""), 0666)
633 if err != nil {
634 t.Fatal(err)
635 }
636
637 target = filepath.Join(target, file[len(filepath.VolumeName(file)):])
638
639 filelink := filepath.Join(tmpdir, "filelink")
640 output, err = exec.Command("cmd", "/c", "mklink", filelink, target).CombinedOutput()
641 if err != nil {
642 t.Fatalf("failed to run mklink %v %v: %v %q", filelink, target, err, output)
643 }
644
645 got, err = filepath.EvalSymlinks(filelink)
646 if err != nil {
647 t.Fatal(err)
648 }
649
650 if winreadlinkvolume.Value() == "0" {
651 want = file
652 } else {
653 want = target
654 }
655 if got != want {
656 t.Errorf(`EvalSymlinks(%q): got %q, want %q`, filelink, got, want)
657 }
658 }
659
660 func TestIssue52476(t *testing.T) {
661 tests := []struct {
662 lhs, rhs string
663 want string
664 }{
665 {`..\.`, `C:`, `..\C:`},
666 {`..`, `C:`, `..\C:`},
667 {`.`, `:`, `.\:`},
668 {`.`, `C:`, `.\C:`},
669 {`.`, `C:/a/b/../c`, `.\C:\a\c`},
670 {`.`, `\C:`, `.\C:`},
671 {`C:\`, `.`, `C:\`},
672 {`C:\`, `C:\`, `C:\C:`},
673 {`C`, `:`, `C\:`},
674 {`\.`, `C:`, `\C:`},
675 {`\`, `C:`, `\C:`},
676 }
677
678 for _, test := range tests {
679 got := filepath.Join(test.lhs, test.rhs)
680 if got != test.want {
681 t.Errorf(`Join(%q, %q): got %q, want %q`, test.lhs, test.rhs, got, test.want)
682 }
683 }
684 }
685
686 func TestAbsWindows(t *testing.T) {
687 for _, test := range []struct {
688 path string
689 want string
690 }{
691 {`C:\foo`, `C:\foo`},
692 {`\\host\share\foo`, `\\host\share\foo`},
693 {`\\host`, `\\host`},
694 {`\\.\NUL`, `\\.\NUL`},
695 {`NUL`, `\\.\NUL`},
696 {`COM1`, `\\.\COM1`},
697 {`a/NUL`, `\\.\NUL`},
698 } {
699 got, err := filepath.Abs(test.path)
700 if err != nil || got != test.want {
701 t.Errorf("Abs(%q) = %q, %v; want %q, nil", test.path, got, err, test.want)
702 }
703 }
704 }
705
View as plain text