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 */
77static
78KTX_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 */
118static
119KTX_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 */
161static
162KTX_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 */
195static
196KTX_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 */
246static
247KTX_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 */
299static
300KTX_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 */
354KTX_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 */
385void
386ktxFileStream_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