2 +----------------------------------------------------------------------+
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
17 #include "hphp/runtime/base/file.h"
19 #include "hphp/runtime/base/array-init.h"
20 #include "hphp/runtime/base/array-iterator.h"
21 #include "hphp/runtime/base/builtin-functions.h"
22 #include "hphp/runtime/base/exceptions.h"
23 #include "hphp/runtime/base/runtime-error.h"
24 #include "hphp/runtime/base/runtime-option.h"
25 #include "hphp/runtime/base/stream-wrapper-registry.h"
26 #include "hphp/runtime/base/string-buffer.h"
27 #include "hphp/runtime/base/request-info.h"
28 #include "hphp/runtime/base/zend-printf.h"
29 #include "hphp/runtime/base/zend-string.h"
31 #include "hphp/runtime/ext/stream/ext_stream.h"
33 #include "hphp/runtime/server/static-content-cache.h"
34 #include "hphp/runtime/server/virtual-host.h"
36 #include "hphp/runtime/base/file-util.h"
37 #include "hphp/util/logger.h"
38 #include "hphp/util/process.h"
40 #include <folly/String.h>
41 #include <folly/portability/Fcntl.h>
42 #include <folly/portability/SysFile.h>
48 const int FileData::DEFAULT_CHUNK_SIZE
= 8192;
50 FileData::FileData(bool nonblocking
)
51 : m_nonblocking(nonblocking
)
54 bool FileData::closeImpl() {
55 if (m_buffer
!= nullptr) {
62 FileData::~FileData() {
63 FileData::closeImpl();
66 ///////////////////////////////////////////////////////////////////////////////
69 StaticString
File::s_resource_name("stream");
71 RDS_LOCAL(int, s_pcloseRet
);
73 const int File::USE_INCLUDE_PATH
= 1;
75 String
File::TranslatePathKeepRelative(const char* filename
, uint32_t size
) {
76 // canonicalize asserts that we don't have nulls
77 String canonicalized
= FileUtil::canonicalize(filename
, size
);
78 if (RID().hasSafeFileAccess()) {
79 auto const& allowedDirectories
= RID().getAllowedDirectoriesProcessed();
80 auto it
= std::upper_bound(allowedDirectories
.begin(),
81 allowedDirectories
.end(), canonicalized
,
82 [](const String
& val
, const std::string
& dir
) {
83 return strcmp(val
.c_str(), dir
.c_str()) < 0;
85 if (it
!= allowedDirectories
.begin()) {
86 const std::string
& dir
= *--it
;
87 if (dir
.size() <= canonicalized
.size() &&
88 !strncmp(dir
.c_str(), canonicalized
.c_str(), dir
.size())) {
93 // disallow access with an absolute path
94 if (FileUtil::isAbsolutePath(canonicalized
.slice())) {
95 return empty_string();
98 // unresolvable paths are all considered as unsafe
99 if (canonicalized
.find("..") >= 0) {
100 assertx(canonicalized
.find("..") == 0);
101 return empty_string();
105 return canonicalized
;
108 String
File::TranslatePath(const String
& filename
) {
109 if (filename
.empty()) {
110 // Special case: an empty string should continue to be an empty string.
111 // Otherwise it would be canonicalized to CWD, which is inconsistent with
112 // PHP and most filesystem utilities.
114 } else if (!FileUtil::isAbsolutePath(filename
.slice())) {
115 String cwd
= g_context
->getCwd();
116 return TranslatePathKeepRelative(cwd
+ "/" + filename
);
118 return TranslatePathKeepRelative(filename
);
122 String
File::TranslatePathWithFileCache(const String
& filename
) {
123 // canonicalize asserts that we don't have nulls
124 String canonicalized
= FileUtil::canonicalize(filename
);
125 String translated
= TranslatePath(canonicalized
);
126 if (!translated
.empty() && access(translated
.data(), F_OK
) < 0 &&
127 StaticContentCache::TheFileCache
) {
128 if (StaticContentCache::TheFileCache
->exists(canonicalized
.data(),
130 // we use file cache's file name to make stat() work
131 translated
= String(RuntimeOption::FileCache
);
137 String
File::TranslateCommand(const String
& cmd
) {
138 //TODO: security checking
142 bool File::IsVirtualDirectory(const String
& filename
) {
144 StaticContentCache::TheFileCache
&&
145 StaticContentCache::TheFileCache
->dirExists(filename
.data(), false);
148 bool File::IsVirtualFile(const String
& filename
) {
150 StaticContentCache::TheFileCache
&&
151 StaticContentCache::TheFileCache
->fileExists(filename
.data(), false);
154 req::ptr
<File
> File::Open(const String
& filename
, const String
& mode
,
155 int options
/* = 0 */,
156 const req::ptr
<StreamContext
>& context
/* = null */) {
157 Stream::Wrapper
*wrapper
= Stream::getWrapperFromURI(filename
);
158 if (!wrapper
) return nullptr;
159 if (filename
.find('\0') >= 0) return nullptr;
160 auto rcontext
= context
? context
: g_context
->getStreamContext();
161 auto file
= wrapper
->open(filename
, mode
, options
, rcontext
);
163 file
->m_data
->m_name
= filename
.data();
164 file
->m_streamContext
= rcontext
;
165 // Let the wrapper set the mode itself if needed.
166 if (file
->m_data
->m_mode
.empty()) {
167 file
->m_data
->m_mode
= mode
.data();
173 ///////////////////////////////////////////////////////////////////////////////
174 // constructor and destructor
177 std::shared_ptr
<FileData
> data
,
178 const String
& wrapper_type
, /* = null_string */
179 const String
& stream_type
/* = empty_string_ref*/)
181 m_wrapperType(wrapper_type
.get()),
182 m_streamType(stream_type
.get())
185 File::File(bool nonblocking
/* = true */,
186 const String
& wrapper_type
/* = null_string */,
187 const String
& stream_type
/* = empty_string_ref */)
188 : File(std::make_shared
<FileData
>(nonblocking
), wrapper_type
, stream_type
)
192 if(m_data
.unique()) {
199 // Clear non-request-local state without deleting `this`. Therefore assumes
200 // `this` has been request-heap allocated. Note that the derived class'
201 // sweep() is responsible for closing m_fd and any other non-request
202 // resources it might have allocated.
206 m_wrapperType
= nullptr;
207 m_streamType
= nullptr;
210 bool File::closeImpl() {
211 return m_data
? m_data
->closeImpl() : true;
214 ///////////////////////////////////////////////////////////////////////////////
215 // default implementation of virtual functions
218 if (m_data
->m_writepos
> m_data
->m_readpos
) {
219 m_data
->m_position
++;
220 return m_data
->m_buffer
[m_data
->m_readpos
++] & 0xff;
224 int64_t len
= readImpl(buffer
, 1);
228 m_data
->m_position
+= len
;
229 return (int)(unsigned char)buffer
[0];
232 String
File::read() {
235 int64_t avail
= bufferedLen();
237 while (!eof() || avail
) {
238 if (m_data
->m_buffer
== nullptr) {
239 m_data
->m_buffer
= (char *)malloc(m_data
->m_chunkSize
);
240 m_data
->m_bufferSize
= m_data
->m_chunkSize
;
244 sb
.append(m_data
->m_buffer
+ m_data
->m_readpos
, avail
);
248 m_data
->m_writepos
= readImpl(m_data
->m_buffer
, m_data
->m_bufferSize
);
249 m_data
->m_readpos
= 0;
250 avail
= bufferedLen();
257 m_data
->m_position
+= copied
;
261 String
File::read(int64_t length
) {
263 raise_notice("Invalid length %" PRId64
, length
);
264 // XXX: Changing this to empty_string causes problems, something is
265 // writing to this upstream but I'm not sure what and since it's
266 // unlikely to provide significant gain alone I'm leaving it for now.
270 auto const allocSize
= length
;
271 String s
= String(allocSize
, ReserveString
);
272 char *ret
= s
.mutableData();
274 int64_t avail
= bufferedLen();
276 while (avail
< length
&& !eof()) {
277 if (m_data
->m_buffer
== nullptr) {
278 m_data
->m_buffer
= (char *)malloc(m_data
->m_chunkSize
);
279 m_data
->m_bufferSize
= m_data
->m_chunkSize
;
283 memcpy(ret
+ copied
, m_data
->m_buffer
+ m_data
->m_readpos
, avail
);
288 m_data
->m_writepos
= readImpl(m_data
->m_buffer
, m_data
->m_bufferSize
);
289 m_data
->m_readpos
= 0;
290 avail
= bufferedLen();
292 if (avail
== 0 || m_data
->m_nonblocking
) {
293 // For nonblocking mode, temporary out of data.
298 avail
= bufferedLen();
300 int64_t n
= length
< avail
? length
: avail
;
301 memcpy(ret
+ copied
, m_data
->m_buffer
+ m_data
->m_readpos
, n
);
302 m_data
->m_readpos
+= n
;
306 m_data
->m_position
+= copied
;
308 assertx(copied
<= allocSize
);
313 int64_t File::write(const String
& data
, int64_t length
/* = 0 */) {
315 int64_t offset
= m_data
->m_readpos
- m_data
->m_writepos
;
316 // Writing shouldn't change the EOF status, but because we have a
317 // transparent buffer, we need to do read operations on the backing
320 // EOF state isn't just a matter of position on all subclasses;
321 // even seek(0, SEEK_CUR) can change it.
322 auto eof
= m_data
->m_eof
;
323 m_data
->m_readpos
= m_data
->m_writepos
= 0; // invalidating read buffer
324 seek(offset
, SEEK_CUR
);
328 if (length
<= 0 || length
> data
.size()) {
329 length
= data
.size();
336 int64_t written
= writeImpl(data
.data(), length
);
337 m_data
->m_position
+= written
;
341 int File::putc(char c
) {
344 int ret
= writeImpl(buf
, 1);
345 m_data
->m_position
+= ret
;
349 bool File::seek(int64_t offset
, int whence
/* = SEEK_SET */) {
350 if (whence
!= SEEK_CUR
) {
351 throw_not_supported(__func__
, "cannot seek other than SEEK_CUR");
354 throw_not_supported(__func__
, "cannot seek backwards");
357 int64_t avail
= bufferedLen();
359 if (avail
>= offset
) {
360 m_data
->m_readpos
+= offset
;
364 m_data
->m_readpos
+= avail
;
370 int64_t nread
= offset
> (int64_t)sizeof(tmp
) ? (int64_t)sizeof(tmp
) : offset
;
371 nread
= readImpl(tmp
, nread
);
381 bool File::setBlocking(bool mode
) {
382 int flags
= fcntl(fd(), F_GETFL
, 0);
384 flags
&= ~O_NONBLOCK
;
388 return fcntl(fd(), F_SETFL
, flags
) != -1;
391 bool File::setTimeout(uint64_t /*usecs*/) {
395 int64_t File::tell() {
396 throw_not_supported(__func__
, "cannot tell");
400 throw_not_supported(__func__
, "cannot test eof");
403 bool File::rewind() {
404 throw_not_supported(__func__
, "cannot rewind");
411 bool File::truncate(int64_t /*size*/) {
412 throw_not_supported(__func__
, "cannot truncate");
415 bool File::lock(int operation
) {
417 return lock(operation
, b
);
420 bool File::lock(int operation
, bool &wouldblock
/* = false */) {
421 assertx(m_data
->m_fd
>= 0);
424 if (flock(m_data
->m_fd
, operation
)) {
425 if (errno
== EWOULDBLOCK
) {
433 bool File::stat(struct stat
* /*sb*/) {
434 // Undocumented, but Zend returns false for streams where fstat is unsupported
439 s_wrapper_type("wrapper_type"),
440 s_stream_type("stream_type"),
442 s_unread_bytes("unread_bytes"),
443 s_seekable("seekable"),
445 s_timed_out("timed_out"),
446 s_blocked("blocked"),
448 s_wrapper_data("wrapper_data");
450 Array
File::getMetaData() {
452 s_wrapper_type
, getWrapperType(),
453 s_stream_type
, getStreamType(),
454 s_mode
, String(m_data
->m_mode
),
456 s_seekable
, seekable(),
457 s_uri
, String(m_data
->m_name
),
461 s_wrapper_data
, getWrapperMetaData()
465 String
File::getWrapperType() const {
466 if (!m_wrapperType
|| m_wrapperType
->empty()) {
467 return o_getClassName();
469 return String
{m_wrapperType
};
472 ///////////////////////////////////////////////////////////////////////////////
475 String
File::readLine(int64_t maxlen
/* = 0 */) {
476 size_t current_buf_size
= 0;
477 size_t total_copied
= 0;
480 int64_t avail
= bufferedLen();
485 char *readptr
= m_data
->m_buffer
+ m_data
->m_readpos
;
486 const char *eol
= nullptr;
489 cr
= (const char *)memchr(readptr
, '\r', avail
);
490 lf
= (const char *)memchr(readptr
, '\n', avail
);
491 if (cr
&& lf
!= cr
+ 1 && !(lf
&& lf
< cr
) && cr
!= &readptr
[avail
- 1]) {
494 } else if ((cr
&& lf
&& cr
== lf
- 1) || (lf
)) {
495 /* dos or unix endings */
497 } else if (cr
!= &readptr
[avail
- 1]) {
502 cpysz
= eol
- readptr
+ 1;
507 if (maxlen
> 0 && maxlen
<= cpysz
) {
512 current_buf_size
+= cpysz
+ 1;
514 ret
= (char *)realloc(ret
, current_buf_size
);
516 ret
= (char *)malloc(current_buf_size
);
518 memcpy(ret
+ total_copied
, readptr
, cpysz
);
520 m_data
->m_position
+= cpysz
;
521 m_data
->m_readpos
+= cpysz
;
523 total_copied
+= cpysz
;
531 if (m_data
->m_buffer
== nullptr) {
532 m_data
->m_buffer
= (char *)malloc(m_data
->m_chunkSize
);
533 m_data
->m_bufferSize
= m_data
->m_chunkSize
;
535 m_data
->m_writepos
= readImpl(m_data
->m_buffer
, m_data
->m_bufferSize
);
536 m_data
->m_readpos
= 0;
537 if (bufferedLen() == 0) {
543 if (total_copied
== 0) {
544 assertx(ret
== nullptr);
548 ret
[total_copied
] = '\0';
549 return String(ret
, total_copied
, AttachString
);
552 Variant
File::readRecord(const String
& delimiter
, int64_t maxlen
/* = 0 */) {
553 if (eof() && m_data
->m_writepos
== m_data
->m_readpos
) {
557 if (maxlen
<= 0 || maxlen
> m_data
->m_chunkSize
) {
558 maxlen
= m_data
->m_chunkSize
;
561 int64_t avail
= bufferedLen();
562 if (m_data
->m_buffer
== nullptr) {
563 m_data
->m_buffer
= (char *)malloc(m_data
->m_chunkSize
* 3);
564 m_data
->m_bufferSize
= m_data
->m_chunkSize
* 3;
565 } else if (m_data
->m_bufferSize
< m_data
->m_chunkSize
* 3) {
566 auto newbuf
= malloc(m_data
->m_chunkSize
* 3);
567 memcpy(newbuf
, m_data
->m_buffer
, m_data
->m_bufferSize
);
568 free(m_data
->m_buffer
);
569 m_data
->m_buffer
= (char*) newbuf
;
570 m_data
->m_bufferSize
= m_data
->m_chunkSize
* 3;
573 if (avail
< maxlen
&& !eof()) {
574 assertx(m_data
->m_writepos
+ maxlen
- avail
<= m_data
->m_chunkSize
* 3);
575 m_data
->m_writepos
+=
576 readImpl(m_data
->m_buffer
+ m_data
->m_writepos
, maxlen
- avail
);
577 maxlen
= bufferedLen();
579 if (m_data
->m_readpos
>= m_data
->m_chunkSize
) {
580 memcpy(m_data
->m_buffer
,
581 m_data
->m_buffer
+ m_data
->m_readpos
,
583 m_data
->m_writepos
-= m_data
->m_readpos
;
584 m_data
->m_readpos
= 0;
590 if (delimiter
.empty()) {
593 if (delimiter
.size() == 1) {
594 e
= (const char *)memchr(m_data
->m_buffer
+ m_data
->m_readpos
,
598 int64_t pos
= string_find(m_data
->m_buffer
+ m_data
->m_readpos
,
605 e
= m_data
->m_buffer
+ m_data
->m_readpos
+ pos
;
614 toread
= e
- m_data
->m_buffer
- m_data
->m_readpos
;
619 if (toread
> maxlen
&& maxlen
> 0) {
624 String s
= String(toread
, ReserveString
);
625 char *buf
= s
.mutableData();
627 memcpy(buf
, m_data
->m_buffer
+ m_data
->m_readpos
, toread
);
630 m_data
->m_readpos
+= toread
;
631 m_data
->m_position
+= toread
;
633 m_data
->m_readpos
+= delimiter
.size();
634 m_data
->m_position
+= delimiter
.size();
640 return empty_string();
643 int64_t File::print() {
647 int64_t len
= readImpl(buffer
, 1024);
650 g_context
->write(buffer
, len
);
655 int64_t File::printf(const String
& format
, const Array
& args
) {
656 String str
= string_printf(format
.data(), format
.size(), args
);
660 const StaticString
s_Unknown("Unknown");
661 const String
& File::o_getResourceName() const {
662 if (isInvalid()) return s_Unknown
;
663 return s_resource_name
;
666 int64_t File::getChunkSize() const{
667 return m_data
->m_chunkSize
;
670 void File::setChunkSize(int64_t chunk_size
) {
672 assertx(chunk_size
> 0);
674 m_data
->m_chunkSize
= chunk_size
;
676 if (m_data
->m_buffer
!= nullptr && m_data
->m_chunkSize
> m_data
->m_bufferSize
) {
677 m_data
->m_buffer
= (char *)realloc(m_data
->m_buffer
, m_data
->m_chunkSize
);
678 m_data
->m_bufferSize
= m_data
->m_chunkSize
;
682 ///////////////////////////////////////////////////////////////////////////////
685 int64_t File::writeCSV(const Array
& fields
, char delimiter_char
/* = ',' */,
686 char enclosure_char
/* = '"' */,
687 char escape_char
/* = '\' */) {
689 int count
= fields
.size();
690 StringBuffer
csvline(1024);
692 for (ArrayIter
iter(fields
); iter
; ++iter
) {
693 String value
= iter
.second().toString();
694 bool need_enclosure
= false;
695 for (int i
= 0; i
< value
.size(); i
++) {
696 char ch
= value
.charAt(i
);
697 if (ch
== delimiter_char
|| ch
== enclosure_char
|| ch
== escape_char
||
698 ch
== '\n' || ch
== '\r' || ch
== '\t' || ch
== ' ') {
699 need_enclosure
= true;
703 if (need_enclosure
) {
704 csvline
.append(enclosure_char
);
705 const char *ch
= value
.data();
706 const char *end
= ch
+ value
.size();
707 bool escaped
= false;
709 if (*ch
== escape_char
) {
711 } else if (!escaped
&& *ch
== enclosure_char
) {
712 csvline
.append(enclosure_char
);
719 csvline
.append(enclosure_char
);
721 csvline
.append(value
);
724 if (++line
!= count
) {
725 csvline
.append(delimiter_char
);
728 csvline
.append('\n');
730 return write(csvline
.detach());
733 static const char *lookup_trailing_spaces(const char *ptr
, int len
) {
736 switch (*(ptr
- 1)) {
738 if (len
> 1 && *(ptr
- 2) == '\r') {
741 /* break is omitted intentionally */
749 Array
File::readCSV(int64_t length
/* = 0 */,
750 char delimiter_char
/* = ',' */,
751 char enclosure_char
/* = '"' */,
752 char escape_char
/* = '\\' */,
753 const String
* input
/* = nullptr */) {
754 const String
& line
= (input
!= nullptr) ? *input
: readLine(length
);
760 const char *buf
= line
.data();
761 int64_t buf_len
= line
.size();
763 char *temp
, *tptr
, *line_end
, *limit
;
766 int64_t temp_len
, line_end_len
;
767 bool first_field
= true;
770 /* Now into new section that parses buf for delimiter/enclosure fields */
772 /* Strip trailing space from buf, saving end of line in case required
773 for enclosure field */
775 tptr
= (char *)lookup_trailing_spaces(buf
, buf_len
);
776 line_end_len
= buf_len
- (size_t)(tptr
- buf
);
777 line_end
= limit
= tptr
;
779 /* reserve workspace for building each individual field */
781 temp
= (char *)malloc(temp_len
+ line_end_len
+ 1);
783 /* Initialize return array */
784 auto ret
= Array::CreateVArray();
786 /* Main loop to read CSV fields */
787 /* NB this routine will return a single null entry for a blank line */
790 const char *hunk_begin
;
794 /* 1. Strip any leading space before an enclosure */
796 inc_len
= (bptr
< limit
);
797 const char *tmp
= bptr
;
798 while ((*tmp
!= delimiter_char
) && isspace((int)*(unsigned char *)tmp
)) {
801 if (*tmp
== enclosure_char
) {
805 if (first_field
&& bptr
== line_end
) {
806 ret
.append(uninit_variant
);
811 /* 2. Read field, leaving bptr pointing at start of next field */
812 if (inc_len
!= 0 && *bptr
== enclosure_char
) {
815 bptr
++; /* move on to first character in field */
818 /* 2A. handle enclosure delimited field */
825 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
- 1);
826 tptr
+= (bptr
- hunk_begin
- 1);
831 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
);
832 tptr
+= (bptr
- hunk_begin
);
834 /* break is omitted intentionally */
837 if (hunk_begin
!= line_end
) {
838 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
);
839 tptr
+= (bptr
- hunk_begin
);
842 /* add the embedded line end to the field */
843 memcpy(tptr
, line_end
, line_end_len
);
844 tptr
+= line_end_len
;
846 new_line
= (input
!= nullptr) ? String() : readLine(length
);
847 const char *new_buf
= new_line
.data();
848 int64_t new_len
= new_line
.size();
850 /* we've got an unterminated enclosure,
851 * assign all the data from the start of
852 * the enclosure to end of data to the
854 if ((size_t)temp_len
> (size_t)(limit
- buf
)) {
860 char *new_temp
= (char*)realloc(temp
, temp_len
);
861 tptr
= new_temp
+ (size_t)(tptr
- temp
);
865 bptr
= buf
= new_buf
;
868 line_end
= limit
= (char *)lookup_trailing_spaces(buf
, buf_len
);
869 line_end_len
= buf_len
- (size_t)(limit
- buf
);
876 /* we need to determine if the enclosure is
877 * 'real' or is it escaped */
879 case 1: /* escaped */
883 case 2: /* embedded enclosure ? let's check it */
884 if (*bptr
!= enclosure_char
) {
886 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
- 1);
887 tptr
+= (bptr
- hunk_begin
- 1);
891 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
);
892 tptr
+= (bptr
- hunk_begin
);
898 if (*bptr
== enclosure_char
) {
900 } else if (*bptr
== escape_char
) {
908 inc_len
= (bptr
< limit
? 1 : 0);
912 /* look up for a delimiter */
920 if (*bptr
== delimiter_char
) {
928 inc_len
= (bptr
< limit
? 1 : 0);
932 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
);
933 tptr
+= (bptr
- hunk_begin
);
937 /* 3B. Handle non-enclosure field */
946 if (*bptr
== delimiter_char
) {
954 inc_len
= (bptr
< limit
? 1 : 0);
958 memcpy(tptr
, hunk_begin
, bptr
- hunk_begin
);
959 tptr
+= (bptr
- hunk_begin
);
961 comp_end
= (char *)lookup_trailing_spaces(temp
, tptr
- temp
);
962 if (*bptr
== delimiter_char
) {
967 /* 3. Now pass our field back to php */
969 ret
.append(String(temp
, comp_end
- temp
, CopyString
));
970 } while (inc_len
> 0);
977 String
File::getLastError() {
978 return String(folly::errnoStr(errno
));
981 ///////////////////////////////////////////////////////////////////////////////