Fix the behaviour of FileGroup when seeking too far.
[dcpomatic.git] / src / lib / file_group.cc
1 /*
2     Copyright (C) 2013-2018 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 /** @file  src/lib/file_group.cc
22  *  @brief FileGroup class.
23  */
24
25 #include "file_group.h"
26 #include "exceptions.h"
27 #include "cross.h"
28 #include "compose.hpp"
29 #include <sndfile.h>
30 #include <cstdio>
31 #include <iostream>
32
33 using std::vector;
34 using std::cout;
35
36 /** Construct a FileGroup with no files */
37 FileGroup::FileGroup ()
38         : _current_path (0)
39         , _current_file (0)
40         , _current_size (0)
41 {
42
43 }
44
45 /** Construct a FileGroup with a single file */
46 FileGroup::FileGroup (boost::filesystem::path p)
47         : _current_path (0)
48         , _current_file (0)
49         , _current_size (0)
50 {
51         _paths.push_back (p);
52         ensure_open_path (0);
53         seek (0, SEEK_SET);
54 }
55
56 /** Construct a FileGroup with multiple files */
57 FileGroup::FileGroup (vector<boost::filesystem::path> const & p)
58         : _paths (p)
59         , _current_path (0)
60         , _current_file (0)
61 {
62         ensure_open_path (0);
63         seek (0, SEEK_SET);
64 }
65
66 /** Destroy a FileGroup, closing any open file */
67 FileGroup::~FileGroup ()
68 {
69         if (_current_file) {
70                 fclose (_current_file);
71         }
72 }
73
74 void
75 FileGroup::set_paths (vector<boost::filesystem::path> const & p)
76 {
77         _paths = p;
78         ensure_open_path (0);
79         seek (0, SEEK_SET);
80 }
81
82 /** Ensure that the given path index in the content is the _current_file */
83 void
84 FileGroup::ensure_open_path (size_t p) const
85 {
86         if (_current_file && _current_path == p) {
87                 /* Already open */
88                 return;
89         }
90
91         if (_current_file) {
92                 fclose (_current_file);
93         }
94
95         _current_path = p;
96         _current_file = fopen_boost (_paths[_current_path], "rb");
97         if (_current_file == 0) {
98                 throw OpenFileError (_paths[_current_path], errno, OpenFileError::READ);
99         }
100         _current_size = boost::filesystem::file_size (_paths[_current_path]);
101 }
102
103 int64_t
104 FileGroup::seek (int64_t pos, int whence) const
105 {
106         /* Convert pos to `full_pos', which is an offset from the start
107            of all the files.
108         */
109         int64_t full_pos = 0;
110         switch (whence) {
111         case SEEK_SET:
112                 full_pos = pos;
113                 break;
114         case SEEK_CUR:
115                 for (size_t i = 0; i < _current_path; ++i) {
116                         full_pos += boost::filesystem::file_size (_paths[i]);
117                 }
118 #ifdef DCPOMATIC_WINDOWS
119                 full_pos += _ftelli64 (_current_file);
120 #else
121                 full_pos += ftell (_current_file);
122 #endif
123                 full_pos += pos;
124                 break;
125         case SEEK_END:
126                 full_pos = length() - pos;
127                 break;
128         }
129
130         /* Seek to full_pos */
131         size_t i = 0;
132         int64_t sub_pos = full_pos;
133         while (i < _paths.size ()) {
134                 boost::uintmax_t len = boost::filesystem::file_size (_paths[i]);
135                 if (sub_pos < int64_t (len)) {
136                         break;
137                 }
138                 ++i;
139                 if (i < _paths.size()) {
140                         /* If we've run out of files we need to seek off the end of the last file */
141                         sub_pos -= len;
142                 }
143         }
144
145         if (i == _paths.size ()) {
146                 /* Seeking too far isn't an error; we'll seek too far in the last file which
147                  * will "pass on" fseek()'s behaviour to our caller.
148                  */
149                 i--;
150         }
151
152         ensure_open_path (i);
153         dcpomatic_fseek (_current_file, sub_pos, SEEK_SET);
154         return full_pos;
155 }
156
157 /** Try to read some data from the current position into a buffer.
158  *  @param buffer Buffer to write data into.
159  *  @param amount Number of bytes to read.
160  *  @return Number of bytes read.
161  */
162 int
163 FileGroup::read (uint8_t* buffer, int amount) const
164 {
165         int read = 0;
166         while (true) {
167                 int64_t to_read = amount - read;
168 #ifdef DCPOMATIC_WINDOWS
169                 /* If we over-read from the file by too much on Windows we get a errno=22 rather than an feof condition,
170                  * for unknown reasons.  So if we're going to over-read, we need to do it by a little bit, so that feof
171                  * still gets triggered but there is no errno=22.
172                  */
173                 to_read = std::min(to_read, static_cast<int64_t>(_current_size - _ftelli64(_current_file) + 1));
174 #endif
175                 int const this_time = fread (buffer + read, 1, to_read, _current_file);
176                 read += this_time;
177                 if (read == amount) {
178                         /* Done */
179                         break;
180                 }
181
182                 if (ferror(_current_file)) {
183                         throw FileError (String::compose("fread error %1", errno), _paths[_current_path]);
184                 }
185
186                 if (feof (_current_file)) {
187                         /* See if there is another file to use */
188                         if ((_current_path + 1) >= _paths.size()) {
189                                 break;
190                         }
191                         ensure_open_path (_current_path + 1);
192                 }
193         }
194
195         return read;
196 }
197
198 /** @return Combined length of all the files */
199 int64_t
200 FileGroup::length () const
201 {
202         int64_t len = 0;
203         for (size_t i = 0; i < _paths.size(); ++i) {
204                 len += boost::filesystem::file_size (_paths[i]);
205         }
206
207         return len;
208 }