1
2
3
4
5 package windows
6
7 import (
8 "internal/oserror"
9 "internal/stringslite"
10 "runtime"
11 "structs"
12 "syscall"
13 "unsafe"
14 )
15
16
17 const (
18 O_DIRECTORY = 0x04000
19 )
20
21
22
23
24
25
26
27
28
29 const (
30 O_NOFOLLOW_ANY = 0x200000000
31 O_WRITE_ATTRS = 0x800000000
32 )
33
34 func Openat(dirfd syscall.Handle, name string, flag uint64, perm uint32) (_ syscall.Handle, e1 error) {
35 if len(name) == 0 {
36 return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
37 }
38
39 var access, options uint32
40
41 fileFlags := uint32(flag) & FileFlagsMask
42 if fileFlags&^ValidFileFlagsMask != 0 {
43 return syscall.InvalidHandle, oserror.ErrInvalid
44 }
45 if fileFlags&O_FILE_FLAG_OVERLAPPED == 0 {
46 options |= FILE_SYNCHRONOUS_IO_NONALERT
47 }
48 if fileFlags&O_FILE_FLAG_DELETE_ON_CLOSE != 0 {
49 access |= DELETE
50 }
51 setOptionFlag := func(ntFlag, win32Flag uint32) {
52 if fileFlags&win32Flag != 0 {
53 options |= ntFlag
54 }
55 }
56 setOptionFlag(FILE_NO_INTERMEDIATE_BUFFERING, O_FILE_FLAG_NO_BUFFERING)
57 setOptionFlag(FILE_WRITE_THROUGH, O_FILE_FLAG_WRITE_THROUGH)
58 setOptionFlag(FILE_SEQUENTIAL_ONLY, O_FILE_FLAG_SEQUENTIAL_SCAN)
59 setOptionFlag(FILE_RANDOM_ACCESS, O_FILE_FLAG_RANDOM_ACCESS)
60 setOptionFlag(FILE_OPEN_FOR_BACKUP_INTENT, O_FILE_FLAG_BACKUP_SEMANTICS)
61 setOptionFlag(FILE_SESSION_AWARE, O_FILE_FLAG_SESSION_AWARE)
62 setOptionFlag(FILE_DELETE_ON_CLOSE, O_FILE_FLAG_DELETE_ON_CLOSE)
63 setOptionFlag(FILE_OPEN_NO_RECALL, O_FILE_FLAG_OPEN_NO_RECALL)
64 setOptionFlag(FILE_OPEN_REPARSE_POINT, O_FILE_FLAG_OPEN_REPARSE_POINT)
65
66 switch flag & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
67 case syscall.O_RDONLY:
68
69 access |= FILE_GENERIC_READ
70 case syscall.O_WRONLY:
71 access |= FILE_GENERIC_WRITE
72 options |= FILE_NON_DIRECTORY_FILE
73 case syscall.O_RDWR:
74 access |= FILE_GENERIC_READ | FILE_GENERIC_WRITE
75 options |= FILE_NON_DIRECTORY_FILE
76 default:
77
78
79 access |= SYNCHRONIZE
80 }
81 if flag&syscall.O_CREAT != 0 {
82 access |= FILE_GENERIC_WRITE
83 }
84 if fileFlags&O_FILE_FLAG_NO_BUFFERING != 0 {
85
86 access &^= FILE_APPEND_DATA
87 }
88 if flag&syscall.O_APPEND != 0 {
89 access |= FILE_APPEND_DATA
90
91
92 if flag&syscall.O_TRUNC == 0 {
93 access &^= FILE_WRITE_DATA
94 }
95 }
96 if flag&O_DIRECTORY != 0 {
97 options |= FILE_DIRECTORY_FILE
98 access |= FILE_LIST_DIRECTORY
99 }
100 if flag&syscall.O_SYNC != 0 {
101 options |= FILE_WRITE_THROUGH
102 }
103 if flag&O_WRITE_ATTRS != 0 {
104 access |= FILE_WRITE_ATTRIBUTES
105 }
106
107 access |= STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | FILE_READ_EA
108
109 objAttrs := &OBJECT_ATTRIBUTES{}
110 if flag&O_NOFOLLOW_ANY != 0 {
111 objAttrs.Attributes |= OBJ_DONT_REPARSE
112 }
113 if flag&syscall.O_CLOEXEC == 0 {
114 objAttrs.Attributes |= OBJ_INHERIT
115 }
116 if fileFlags&O_FILE_FLAG_POSIX_SEMANTICS == 0 {
117 objAttrs.Attributes |= OBJ_CASE_INSENSITIVE
118 }
119 if err := objAttrs.init(dirfd, name); err != nil {
120 return syscall.InvalidHandle, err
121 }
122
123
124
125
126
127
128 var disposition uint32
129 switch {
130 case flag&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
131 disposition = FILE_CREATE
132 options |= FILE_OPEN_REPARSE_POINT
133 case flag&syscall.O_CREAT == syscall.O_CREAT:
134 disposition = FILE_OPEN_IF
135 default:
136 disposition = FILE_OPEN
137 }
138
139 fileAttrs := uint32(FILE_ATTRIBUTE_NORMAL)
140 if perm&syscall.S_IWRITE == 0 {
141 fileAttrs = FILE_ATTRIBUTE_READONLY
142 }
143
144 var h syscall.Handle
145 var err error
146 if TestOpenatFallback && flag&O_NOFOLLOW_ANY != 0 {
147 err = STATUS_INVALID_PARAMETER
148 } else {
149 err = NtCreateFile(
150 &h,
151 SYNCHRONIZE|access,
152 objAttrs,
153 &IO_STATUS_BLOCK{},
154 nil,
155 fileAttrs,
156 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
157 disposition,
158 FILE_OPEN_FOR_BACKUP_INTENT|options,
159 nil,
160 0,
161 )
162 }
163 if err == STATUS_INVALID_PARAMETER && flag&O_NOFOLLOW_ANY != 0 {
164 h, err = openatFallback(name, SYNCHRONIZE|access, *objAttrs, fileAttrs, disposition, FILE_OPEN_FOR_BACKUP_INTENT|options)
165 }
166 if err != nil {
167 return h, ntCreateFileError(err, flag)
168 }
169
170 if flag&syscall.O_TRUNC != 0 {
171 err = syscall.Ftruncate(h, 0)
172 if err == ERROR_INVALID_PARAMETER {
173
174
175
176 if t, err1 := syscall.GetFileType(h); err1 == nil && (t == syscall.FILE_TYPE_PIPE || t == syscall.FILE_TYPE_CHAR) {
177 err = nil
178 }
179 }
180 if err != nil {
181 syscall.CloseHandle(h)
182 return syscall.InvalidHandle, err
183 }
184 }
185
186 return h, nil
187 }
188
189
190
191 var TestOpenatFallback bool
192
193
194
195
196 func openatFallback(name string, access uint32, attrs OBJECT_ATTRIBUTES, fileAttrs, disposition, options uint32) (syscall.Handle, error) {
197
198
199
200
201
202
203
204
205 if attrs.RootDirectory == 0 || name == ".." ||
206 stringslite.IndexByte(name, '\\') >= 0 || stringslite.IndexByte(name, '/') >= 0 || stringslite.IndexByte(name, ':') >= 0 {
207 return syscall.InvalidHandle, STATUS_INVALID_PARAMETER
208 }
209 attrs.Attributes &^= OBJ_DONT_REPARSE
210 var h syscall.Handle
211 err := NtCreateFile(
212 &h, access, &attrs, &IO_STATUS_BLOCK{}, nil, fileAttrs,
213 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, disposition,
214 (options|FILE_OPEN_REPARSE_POINT)&^FILE_DELETE_ON_CLOSE, nil, 0,
215 )
216 if err != nil {
217 return syscall.InvalidHandle, err
218 }
219
220
221
222 if options&FILE_OPEN_REPARSE_POINT == 0 {
223 var info syscall.ByHandleFileInformation
224 err = syscall.GetFileInformationByHandle(h, &info)
225 if err == nil && info.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
226 err = STATUS_REPARSE_POINT_ENCOUNTERED
227 }
228 if err != nil {
229 syscall.CloseHandle(h)
230 return syscall.InvalidHandle, err
231 }
232 }
233 if options&FILE_DELETE_ON_CLOSE == 0 {
234 return h, nil
235 }
236 defer syscall.CloseHandle(h)
237
238
239
240
241
242 attrs.RootDirectory = h
243 attrs.ObjectName = &NTUnicodeString{}
244 var dh syscall.Handle
245 err = NtOpenFile(
246 &dh, access, &attrs, &IO_STATUS_BLOCK{},
247 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
248 options|FILE_OPEN_REPARSE_POINT,
249 )
250 if err != nil {
251 return syscall.InvalidHandle, err
252 }
253 return dh, nil
254 }
255
256
257 func ntCreateFileError(err error, flag uint64) error {
258 s, ok := err.(NTStatus)
259 if !ok {
260
261 return err
262 }
263 switch s {
264 case STATUS_REPARSE_POINT_ENCOUNTERED:
265 return syscall.ELOOP
266 case STATUS_NOT_A_DIRECTORY:
267
268
269
270
271
272
273
274
275 if flag&O_DIRECTORY != 0 {
276 return syscall.ENOTDIR
277 }
278 case STATUS_FILE_IS_A_DIRECTORY:
279 return syscall.EISDIR
280 case STATUS_OBJECT_NAME_COLLISION:
281 return syscall.EEXIST
282 }
283 return s.Errno()
284 }
285
286 func Mkdirat(dirfd syscall.Handle, name string, mode uint32) error {
287 objAttrs := &OBJECT_ATTRIBUTES{}
288 if err := objAttrs.init(dirfd, name); err != nil {
289 return err
290 }
291 var h syscall.Handle
292 err := NtCreateFile(
293 &h,
294 FILE_GENERIC_READ,
295 objAttrs,
296 &IO_STATUS_BLOCK{},
297 nil,
298 syscall.FILE_ATTRIBUTE_NORMAL,
299 syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
300 FILE_CREATE,
301 FILE_DIRECTORY_FILE,
302 nil,
303 0,
304 )
305 if err != nil {
306 return ntCreateFileError(err, 0)
307 }
308 syscall.CloseHandle(h)
309 return nil
310 }
311
312 func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
313 if name == "." {
314
315
316 return syscall.EINVAL
317 }
318 objAttrs := &OBJECT_ATTRIBUTES{}
319 if err := objAttrs.init(dirfd, name); err != nil {
320 return err
321 }
322 var h syscall.Handle
323 err := NtOpenFile(
324 &h,
325 FILE_READ_ATTRIBUTES|DELETE,
326 objAttrs,
327 &IO_STATUS_BLOCK{},
328 FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
329 FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|options,
330 )
331 if err != nil {
332 if ntStatus, ok := err.(NTStatus); !ok || ntStatus != STATUS_ACCESS_DENIED {
333 return ntCreateFileError(err, 0)
334 }
335
336
337
338
339 err = NtOpenFile(
340 &h,
341 DELETE,
342 objAttrs,
343 &IO_STATUS_BLOCK{},
344 FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
345 FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|options,
346 )
347 if err != nil {
348 return ntCreateFileError(err, 0)
349 }
350 }
351 defer syscall.CloseHandle(h)
352
353 if TestDeleteatFallback {
354 return deleteatFallback(h)
355 }
356
357 const FileDispositionInformationEx = 64
358
359
360
361
362
363
364
365
366
367
368
369 err = NtSetInformationFile(
370 h,
371 &IO_STATUS_BLOCK{},
372 unsafe.Pointer(&FILE_DISPOSITION_INFORMATION_EX{
373 Flags: FILE_DISPOSITION_DELETE |
374 FILE_DISPOSITION_POSIX_SEMANTICS |
375
376
377
378 FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE,
379 }),
380 uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION_EX{})),
381 FileDispositionInformationEx,
382 )
383 switch err {
384 case nil:
385 return nil
386 case STATUS_INVALID_INFO_CLASS,
387 STATUS_INVALID_PARAMETER,
388 STATUS_NOT_SUPPORTED:
389 return deleteatFallback(h)
390 default:
391 return err.(NTStatus).Errno()
392 }
393 }
394
395
396
397 var TestDeleteatFallback bool
398
399
400
401
402 func deleteatFallback(h syscall.Handle) error {
403 var data syscall.ByHandleFileInformation
404 if err := syscall.GetFileInformationByHandle(h, &data); err == nil && data.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
405
406
407 wh, err := ReOpenFile(h,
408 FILE_WRITE_ATTRIBUTES,
409 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
410 syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS,
411 )
412 if err != nil {
413 return err
414 }
415 err = SetFileInformationByHandle(
416 wh,
417 FileBasicInfo,
418 unsafe.Pointer(&FILE_BASIC_INFO{
419 FileAttributes: data.FileAttributes &^ FILE_ATTRIBUTE_READONLY,
420 }),
421 uint32(unsafe.Sizeof(FILE_BASIC_INFO{})),
422 )
423 syscall.CloseHandle(wh)
424 if err != nil {
425 return err
426 }
427 }
428
429 return SetFileInformationByHandle(
430 h,
431 FileDispositionInfo,
432 unsafe.Pointer(&FILE_DISPOSITION_INFO{
433 DeleteFile: 1,
434 }),
435 uint32(unsafe.Sizeof(FILE_DISPOSITION_INFO{})),
436 )
437 }
438
439 func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
440 objAttrs := &OBJECT_ATTRIBUTES{}
441 if err := objAttrs.init(olddirfd, oldpath); err != nil {
442 return err
443 }
444 var h syscall.Handle
445 err := NtOpenFile(
446 &h,
447 SYNCHRONIZE|DELETE,
448 objAttrs,
449 &IO_STATUS_BLOCK{},
450 FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
451 FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
452 )
453 if err != nil {
454 return ntCreateFileError(err, 0)
455 }
456 defer syscall.CloseHandle(h)
457
458 renameInfoEx := FILE_RENAME_INFORMATION_EX{
459 Flags: FILE_RENAME_REPLACE_IF_EXISTS |
460 FILE_RENAME_POSIX_SEMANTICS,
461 RootDirectory: newdirfd,
462 }
463 p16, err := syscall.UTF16FromString(newpath)
464 if err != nil {
465 return err
466 }
467 if len(p16) > len(renameInfoEx.FileName) {
468 return syscall.EINVAL
469 }
470 copy(renameInfoEx.FileName[:], p16)
471 renameInfoEx.FileNameLength = uint32((len(p16) - 1) * 2)
472
473 const (
474 FileRenameInformation = 10
475 FileRenameInformationEx = 65
476 )
477 err = NtSetInformationFile(
478 h,
479 &IO_STATUS_BLOCK{},
480 unsafe.Pointer(&renameInfoEx),
481 uint32(unsafe.Sizeof(FILE_RENAME_INFORMATION_EX{})),
482 FileRenameInformationEx,
483 )
484 if err == nil {
485 return nil
486 }
487
488
489
490
491
492
493 renameInfo := FILE_RENAME_INFORMATION{
494 ReplaceIfExists: 1,
495 RootDirectory: newdirfd,
496 }
497 copy(renameInfo.FileName[:], p16)
498 renameInfo.FileNameLength = renameInfoEx.FileNameLength
499
500 err = NtSetInformationFile(
501 h,
502 &IO_STATUS_BLOCK{},
503 unsafe.Pointer(&renameInfo),
504 uint32(unsafe.Sizeof(FILE_RENAME_INFORMATION{})),
505 FileRenameInformation,
506 )
507 if st, ok := err.(NTStatus); ok {
508 return st.Errno()
509 }
510 return err
511 }
512
513 func Linkat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
514 objAttrs := &OBJECT_ATTRIBUTES{}
515 if err := objAttrs.init(olddirfd, oldpath); err != nil {
516 return err
517 }
518 var h syscall.Handle
519 err := NtOpenFile(
520 &h,
521 SYNCHRONIZE|FILE_WRITE_ATTRIBUTES,
522 objAttrs,
523 &IO_STATUS_BLOCK{},
524 FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
525 FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
526 )
527 if err != nil {
528 return ntCreateFileError(err, 0)
529 }
530 defer syscall.CloseHandle(h)
531
532 linkInfo := FILE_LINK_INFORMATION{
533 RootDirectory: newdirfd,
534 }
535 p16, err := syscall.UTF16FromString(newpath)
536 if err != nil {
537 return err
538 }
539 if len(p16) > len(linkInfo.FileName) {
540 return syscall.EINVAL
541 }
542 copy(linkInfo.FileName[:], p16)
543 linkInfo.FileNameLength = uint32((len(p16) - 1) * 2)
544
545 const (
546 FileLinkInformation = 11
547 )
548 err = NtSetInformationFile(
549 h,
550 &IO_STATUS_BLOCK{},
551 unsafe.Pointer(&linkInfo),
552 uint32(unsafe.Sizeof(FILE_LINK_INFORMATION{})),
553 FileLinkInformation,
554 )
555 if st, ok := err.(NTStatus); ok {
556 return st.Errno()
557 }
558 return err
559 }
560
561
562
563
564
565
566
567
568
569
570
571 type SymlinkatFlags uint
572
573 const (
574 SYMLINKAT_DIRECTORY = SymlinkatFlags(1 << iota)
575 SYMLINKAT_RELATIVE
576 )
577
578 func Symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error {
579
580
581
582
583
584 return withPrivilege("SeCreateSymbolicLinkPrivilege", func() error {
585 return symlinkat(oldname, newdirfd, newname, flags)
586 })
587 }
588
589 func symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error {
590 oldnameu16, err := syscall.UTF16FromString(oldname)
591 if err != nil {
592 return err
593 }
594 oldnameu16 = oldnameu16[:len(oldnameu16)-1]
595
596 var options uint32
597 if flags&SYMLINKAT_DIRECTORY != 0 {
598 options |= FILE_DIRECTORY_FILE
599 } else {
600 options |= FILE_NON_DIRECTORY_FILE
601 }
602
603 objAttrs := &OBJECT_ATTRIBUTES{}
604 if err := objAttrs.init(newdirfd, newname); err != nil {
605 return err
606 }
607 var h syscall.Handle
608 err = NtCreateFile(
609 &h,
610 SYNCHRONIZE|FILE_WRITE_ATTRIBUTES|DELETE,
611 objAttrs,
612 &IO_STATUS_BLOCK{},
613 nil,
614 syscall.FILE_ATTRIBUTE_NORMAL,
615 0,
616 FILE_CREATE,
617 FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options,
618 nil,
619 0,
620 )
621 if err != nil {
622 return ntCreateFileError(err, 0)
623 }
624 defer syscall.CloseHandle(h)
625
626
627 type reparseDataBufferT struct {
628 _ structs.HostLayout
629
630 ReparseTag uint32
631 ReparseDataLength uint16
632 Reserved uint16
633
634 SubstituteNameOffset uint16
635 SubstituteNameLength uint16
636 PrintNameOffset uint16
637 PrintNameLength uint16
638 Flags uint32
639 }
640
641 const (
642 headerSize = uint16(unsafe.Offsetof(reparseDataBufferT{}.SubstituteNameOffset))
643 bufferSize = uint16(unsafe.Sizeof(reparseDataBufferT{}))
644 )
645
646
647 rdbbuf := make([]byte, bufferSize+uint16(2*len(oldnameu16)))
648
649 rdb := (*reparseDataBufferT)(unsafe.Pointer(&rdbbuf[0]))
650 rdb.ReparseTag = syscall.IO_REPARSE_TAG_SYMLINK
651 rdb.ReparseDataLength = uint16(len(rdbbuf)) - uint16(headerSize)
652 rdb.SubstituteNameOffset = 0
653 rdb.SubstituteNameLength = uint16(2 * len(oldnameu16))
654 rdb.PrintNameOffset = 0
655 rdb.PrintNameLength = rdb.SubstituteNameLength
656 if flags&SYMLINKAT_RELATIVE != 0 {
657 rdb.Flags = SYMLINK_FLAG_RELATIVE
658 }
659
660 namebuf := rdbbuf[bufferSize:]
661 copy(namebuf, unsafe.String((*byte)(unsafe.Pointer(&oldnameu16[0])), 2*len(oldnameu16)))
662
663 var bytesReturned uint32
664 err = syscall.DeviceIoControl(
665 h,
666 FSCTL_SET_REPARSE_POINT,
667 &rdbbuf[0],
668 uint32(len(rdbbuf)),
669 nil,
670 0,
671 &bytesReturned,
672 nil)
673 if err != nil {
674
675 const FileDispositionInformation = 13
676 NtSetInformationFile(
677 h,
678 &IO_STATUS_BLOCK{},
679 unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
680 DeleteFile: 1,
681 }),
682 uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
683 FileDispositionInformation,
684 )
685 return err
686 }
687
688 return nil
689 }
690
691
692
693
694 func withPrivilege(privilege string, f func() error) error {
695 runtime.LockOSThread()
696 defer runtime.UnlockOSThread()
697
698 err := ImpersonateSelf(SecurityImpersonation)
699 if err != nil {
700 return f()
701 }
702 defer RevertToSelf()
703
704 curThread, err := GetCurrentThread()
705 if err != nil {
706 return f()
707 }
708 var token syscall.Token
709 err = OpenThreadToken(curThread, syscall.TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES, false, &token)
710 if err != nil {
711 return f()
712 }
713 defer syscall.CloseHandle(syscall.Handle(token))
714
715 privStr, err := syscall.UTF16PtrFromString(privilege)
716 if err != nil {
717 return f()
718 }
719 var tokenPriv TOKEN_PRIVILEGES
720 err = LookupPrivilegeValue(nil, privStr, &tokenPriv.Privileges[0].Luid)
721 if err != nil {
722 return f()
723 }
724
725 tokenPriv.PrivilegeCount = 1
726 tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
727 err = AdjustTokenPrivileges(token, false, &tokenPriv, 0, nil, nil)
728 if err != nil {
729 return f()
730 }
731
732 return f()
733 }
734
View as plain text