1 | /* -*- tab-width: 4; -*- */ |
2 | /* vi: set sw=2 ts=4 expandtab: */ |
3 | |
4 | /* |
5 | * Copyright 2010-2020 The Khronos Group Inc. |
6 | * SPDX-License-Identifier: Apache-2.0 |
7 | */ |
8 | |
9 | /** |
10 | * @file |
11 | * @~English |
12 | * |
13 | * @brief Implementation of ktxStream for FILE. |
14 | * |
15 | * @author Maksim Kolesin, Under Development |
16 | * @author Georg Kolling, Imagination Technology |
17 | * @author Mark Callow, HI Corporation |
18 | */ |
19 | |
20 | #include <assert.h> |
21 | #include <errno.h> |
22 | #include <inttypes.h> |
23 | #include <string.h> |
24 | /* I need these on Linux. Why? */ |
25 | #define __USE_LARGEFILE 1 // For declaration of ftello, etc. |
26 | #define __USE_POSIX 1 // For declaration of fileno. |
27 | #define _POSIX_SOURCE 1 // For both the above in Emscripten. |
28 | #include <stdio.h> |
29 | #include <stdlib.h> |
30 | #include <sys/types.h> // For stat.h on Windows |
31 | #define __USE_MISC 1 // For declaration of S_IF... |
32 | #include <sys/stat.h> |
33 | |
34 | #include "ktx.h" |
35 | #include "ktxint.h" |
36 | #include "filestream.h" |
37 | |
38 | // Gotta love Windows :-( |
39 | #if defined(_MSC_VER) |
40 | #if defined(_WIN64) |
41 | #define ftello _ftelli64 |
42 | #define fseeko _fseeki64 |
43 | #else |
44 | #define ftello ftell |
45 | #define fseeko fseek |
46 | #endif |
47 | #define fileno _fileno |
48 | #define fstat _fstat |
49 | #define stat _stat |
50 | #define S_IFIFO _S_IFIFO |
51 | #define S_IFSOCK 0xC000 |
52 | typedef unsigned short mode_t; |
53 | #endif |
54 | |
55 | #if defined(__MINGW32__) |
56 | #define S_IFSOCK 0xC000 |
57 | #endif |
58 | |
59 | #define KTX_FILE_STREAM_MAX (1 << (sizeof(ktx_off_t) - 1) - 1) |
60 | |
61 | /** |
62 | * @~English |
63 | * @brief Read bytes from a ktxFileStream. |
64 | * |
65 | * @param [in] str pointer to the ktxStream from which to read. |
66 | * @param [out] dst pointer to a block of memory with a size |
67 | * of at least @p size bytes, converted to a void*. |
68 | * @param [in,out] count pointer to total count of bytes to be read. |
69 | * On completion set to number of bytes read. |
70 | * |
71 | * @return KTX_SUCCESS on success, other KTX_* enum values on error. |
72 | * |
73 | * @exception KTX_INVALID_VALUE @p dst is @c NULL or @p src is @c NULL. |
74 | * @exception KTX_FILE_READ_ERROR an error occurred while reading the file. |
75 | * @exception KTX_FILE_UNEXPECTED_EOF not enough data to satisfy the request. |
76 | */ |
77 | static |
78 | KTX_error_code ktxFileStream_read(ktxStream* str, void* dst, const ktx_size_t count) |
79 | { |
80 | ktx_size_t nread; |
81 | |
82 | if (!str || !dst) |
83 | return KTX_INVALID_VALUE; |
84 | |
85 | assert(str->type == eStreamTypeFile); |
86 | |
87 | if ((nread = fread(dst, 1, count, str->data.file)) != count) { |
88 | if (feof(str->data.file)) { |
89 | return KTX_FILE_UNEXPECTED_EOF; |
90 | } else { |
91 | return KTX_FILE_READ_ERROR; |
92 | } |
93 | } |
94 | str->readpos += count; |
95 | |
96 | return KTX_SUCCESS; |
97 | } |
98 | |
99 | /** |
100 | * @~English |
101 | * @brief Skip bytes in a ktxFileStream. |
102 | * |
103 | * @param [in] str pointer to a ktxStream object. |
104 | * @param [in] count number of bytes to be skipped. |
105 | * |
106 | * In order to support applications reading from stdin, read characters |
107 | * rather than using seek functions. |
108 | * |
109 | * @return KTX_SUCCESS on success, other KTX_* enum values on error. |
110 | * |
111 | * @exception KTX_INVALID_VALUE @p str is @c NULL or @p count is less than zero. |
112 | * @exception KTX_INVALID_OPERATION skipping @p count bytes would go beyond EOF. |
113 | * @exception KTX_FILE_READ_ERROR an error occurred while reading the file. |
114 | * @exception KTX_FILE_UNEXPECTED_EOF not enough data to satisfy the request. |
115 | * @p count is set to the number of bytes |
116 | * skipped. |
117 | */ |
118 | static |
119 | KTX_error_code ktxFileStream_skip(ktxStream* str, const ktx_size_t count) |
120 | { |
121 | if (!str) |
122 | return KTX_INVALID_VALUE; |
123 | |
124 | assert(str->type == eStreamTypeFile); |
125 | |
126 | for (ktx_uint32_t i = 0; i < count; i++) { |
127 | int ret = getc(str->data.file); |
128 | if (ret == EOF) { |
129 | if (feof(str->data.file)) { |
130 | return KTX_FILE_UNEXPECTED_EOF; |
131 | } else { |
132 | return KTX_FILE_READ_ERROR; |
133 | } |
134 | } |
135 | } |
136 | str->readpos += count; |
137 | |
138 | return KTX_SUCCESS; |
139 | } |
140 | |
141 | /** |
142 | * @~English |
143 | * @brief Write bytes to a ktxFileStream. |
144 | * |
145 | * @param [in] str pointer to the ktxStream that is the destination of the |
146 | * write. |
147 | * @param [in] src pointer to the array of elements to be written, |
148 | * converted to a const void*. |
149 | * @param [in] size size in bytes of each element to be written. |
150 | * @param [in] count number of elements, each one with a @p size of size |
151 | * bytes. |
152 | * |
153 | * @return KTX_SUCCESS on success, other KTX_* enum values on error. |
154 | * |
155 | * @exception KTX_INVALID_VALUE @p str is @c NULL or @p src is @c NULL. |
156 | * @exception KTX_FILE_OVERFLOW the requested write would caused the file to |
157 | * exceed the maximum supported file size. |
158 | * @exception KTX_FILE_WRITE_ERROR a system error occurred while writing the |
159 | * file. |
160 | */ |
161 | static |
162 | KTX_error_code ktxFileStream_write(ktxStream* str, const void *src, |
163 | const ktx_size_t size, |
164 | const ktx_size_t count) |
165 | { |
166 | if (!str || !src) |
167 | return KTX_INVALID_VALUE; |
168 | |
169 | assert(str->type == eStreamTypeFile); |
170 | |
171 | if (fwrite(src, size, count, str->data.file) != count) { |
172 | if (errno == EFBIG || errno == EOVERFLOW) |
173 | return KTX_FILE_OVERFLOW; |
174 | else |
175 | return KTX_FILE_WRITE_ERROR; |
176 | } |
177 | |
178 | return KTX_SUCCESS; |
179 | } |
180 | |
181 | /** |
182 | * @~English |
183 | * @brief Get the current read/write position in a ktxFileStream. |
184 | * |
185 | * @param [in] str pointer to the ktxStream to query. |
186 | * @param [in,out] off pointer to variable to receive the offset value. |
187 | * |
188 | * @return KTX_SUCCESS on success, other KTX_* enum values on error. |
189 | * |
190 | * @exception KTX_FILE_ISPIPE file descriptor underlying stream is associated |
191 | * with a pipe or FIFO so does not have a |
192 | * file-position indicator. |
193 | * @exception KTX_INVALID_VALUE @p str or @p pos is @c NULL. |
194 | */ |
195 | static |
196 | KTX_error_code ktxFileStream_getpos(ktxStream* str, ktx_off_t* pos) |
197 | { |
198 | ktx_off_t ftellval; |
199 | |
200 | if (!str || !pos) |
201 | return KTX_INVALID_VALUE; |
202 | |
203 | assert(str->type == eStreamTypeFile); |
204 | |
205 | if (str->data.file == stdin) { |
206 | *pos = str->readpos; |
207 | } else { |
208 | /* The cast quiets an Xcode warning when building for "Generic iOS Device". |
209 | * I'm not sure why. |
210 | */ |
211 | ftellval = (ktx_off_t)ftello(str->data.file); |
212 | if (ftellval < 0) { |
213 | switch (errno) { |
214 | case ESPIPE: return KTX_FILE_ISPIPE; |
215 | case EOVERFLOW: return KTX_FILE_OVERFLOW; |
216 | } |
217 | } |
218 | |
219 | *pos = ftellval; |
220 | } |
221 | |
222 | return KTX_SUCCESS; |
223 | } |
224 | |
225 | /** |
226 | * @~English |
227 | * @brief Set the current read/write position in a ktxFileStream. |
228 | * |
229 | * Offset of 0 is the start of the file. This function operates |
230 | * like Linux > 3.1's @c lseek() when it is passed a @c whence |
231 | * of @c SEEK_DATA as it returns an error if the seek would |
232 | * go beyond the end of the file. |
233 | * |
234 | * @param [in] str pointer to the ktxStream whose r/w position is to be set. |
235 | * @param [in] off pointer to the offset value to set. |
236 | * |
237 | * @return KTX_SUCCESS on success, other KTX_* enum values on error. |
238 | * |
239 | * Throws the same exceptions as ktxFileStream_getsize() for the reasons given |
240 | * there plus the following: |
241 | * |
242 | * @exception KTX_INVALID_VALUE @p str is @c NULL. |
243 | * @exception KTX_INVALID_OPERATION @p pos is > the size of the file or an |
244 | * fseek error occurred. |
245 | */ |
246 | static |
247 | KTX_error_code ktxFileStream_setpos(ktxStream* str, ktx_off_t pos) |
248 | { |
249 | ktx_size_t fileSize; |
250 | KTX_error_code result; |
251 | |
252 | if (!str) |
253 | return KTX_INVALID_VALUE; |
254 | |
255 | assert(str->type == eStreamTypeFile); |
256 | |
257 | if (str->data.file == stdin) { |
258 | if (pos > str->readpos) |
259 | return str->skip(str, pos - str->readpos); |
260 | else |
261 | return KTX_FILE_ISPIPE; |
262 | } |
263 | |
264 | result = str->getsize(str, &fileSize); |
265 | |
266 | if (result != KTX_SUCCESS) { |
267 | // Device is likely not seekable. |
268 | return result; |
269 | } |
270 | |
271 | if (pos > (ktx_off_t)fileSize) |
272 | return KTX_INVALID_OPERATION; |
273 | |
274 | if (fseeko(str->data.file, pos, SEEK_SET) < 0) |
275 | return KTX_FILE_SEEK_ERROR; |
276 | else |
277 | return KTX_SUCCESS; |
278 | } |
279 | |
280 | /** |
281 | * @~English |
282 | * @brief Get the size of a ktxFileStream in bytes. |
283 | * |
284 | * @param [in] str pointer to the ktxStream whose size is to be queried. |
285 | * @param [in,out] size pointer to a variable in which size will be written. |
286 | * |
287 | * @return KTX_SUCCESS on success, other KTX_* enum values on error. |
288 | * |
289 | * @exception KTX_FILE_OVERFLOW size is too large to be returned in a |
290 | * @c ktx_size_t. |
291 | * @exception KTX_FILE_ISPIPE file descriptor underlying stream is associated |
292 | * with a pipe or FIFO so does not have a |
293 | * file-position indicator. |
294 | * @exception KTX_FILE_READ_ERROR a system error occurred while getting the |
295 | * size. |
296 | * @exception KTX_INVALID_VALUE @p str or @p size is @c NULL. |
297 | * @exception KTX_INVALID_OPERATION stream is a tty. |
298 | */ |
299 | static |
300 | KTX_error_code ktxFileStream_getsize(ktxStream* str, ktx_size_t* size) |
301 | { |
302 | struct stat statbuf; |
303 | int statret; |
304 | |
305 | if (!str || !size) |
306 | return KTX_INVALID_VALUE; |
307 | |
308 | assert(str->type == eStreamTypeFile); |
309 | |
310 | // Need to flush so that fstat will return the current size. |
311 | // Can ignore return value. The only error that can happen is to tell you |
312 | // it was a NOP because the file is read only. |
313 | #if (defined(_MSC_VER) && _MSC_VER < 1900) || defined(__MINGW64__) && !defined(_UCRT) |
314 | // Bug in VS2013 msvcrt. fflush on FILE open for READ changes file offset |
315 | // to 4096. |
316 | if (str->data.file->_flag & _IOWRT) |
317 | #endif |
318 | (void)fflush(str->data.file); |
319 | statret = fstat(fileno(str->data.file), &statbuf); |
320 | if (statret < 0) { |
321 | switch (errno) { |
322 | case EOVERFLOW: return KTX_FILE_OVERFLOW; |
323 | case EIO: |
324 | default: |
325 | return KTX_FILE_READ_ERROR; |
326 | } |
327 | } |
328 | |
329 | mode_t ftype = statbuf.st_mode & S_IFMT; |
330 | if (ftype == S_IFIFO || ftype == S_IFSOCK) |
331 | return KTX_FILE_ISPIPE; |
332 | |
333 | if (statbuf.st_mode & S_IFCHR) |
334 | return KTX_INVALID_OPERATION; |
335 | |
336 | *size = (ktx_size_t)statbuf.st_size; /* See _getpos for why this cast. */ |
337 | |
338 | return KTX_SUCCESS; |
339 | } |
340 | |
341 | /** |
342 | * @~English |
343 | * @brief Initialize a ktxFileStream. |
344 | * |
345 | * @param [in] str pointer to the ktxStream to initialize. |
346 | * @param [in] file pointer to the underlying FILE object. |
347 | * @param [in] closeFileOnDestruct if not false, stdio file pointer will be closed when ktxStream |
348 | * is destructed. |
349 | * |
350 | * @return KTX_SUCCESS on success, KTX_INVALID_VALUE on error. |
351 | * |
352 | * @exception KTX_INVALID_VALUE @p stream is @c NULL or @p file is @c NULL. |
353 | */ |
354 | KTX_error_code ktxFileStream_construct(ktxStream* str, FILE* file, |
355 | ktx_bool_t closeFileOnDestruct) |
356 | { |
357 | if (!str || !file) |
358 | return KTX_INVALID_VALUE; |
359 | |
360 | str->data.file = file; |
361 | str->readpos = 0; |
362 | str->type = eStreamTypeFile; |
363 | str->read = ktxFileStream_read; |
364 | str->skip = ktxFileStream_skip; |
365 | str->write = ktxFileStream_write; |
366 | str->getpos = ktxFileStream_getpos; |
367 | str->setpos = ktxFileStream_setpos; |
368 | str->getsize = ktxFileStream_getsize; |
369 | str->destruct = ktxFileStream_destruct; |
370 | str->closeOnDestruct = closeFileOnDestruct; |
371 | |
372 | return KTX_SUCCESS; |
373 | } |
374 | |
375 | /** |
376 | * @~English |
377 | * @brief Destruct the stream, potentially closing the underlying FILE. |
378 | * |
379 | * This only closes the underyling FILE if the @c closeOnDestruct parameter to |
380 | * ktxFileStream_construct() was not @c KTX_FALSE. |
381 | * |
382 | * @param [in] str pointer to the ktxStream whose FILE is to potentially |
383 | * be closed. |
384 | */ |
385 | void |
386 | ktxFileStream_destruct(ktxStream* str) |
387 | { |
388 | assert(str && str->type == eStreamTypeFile); |
389 | |
390 | if (str->closeOnDestruct) |
391 | fclose(str->data.file); |
392 | str->data.file = 0; |
393 | } |
394 | |