Iso9660ArchivePlugin.cxx 7.73 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2021 The Music Player Daemon Project
3
 * http://www.musicpd.org
4 5 6 7 8 9 10 11 12 13
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
14 15 16 17
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 19 20 21 22 23
 */

/**
  * iso archive handling (requires cdio, and iso9660)
  */

24
#include "Iso9660ArchivePlugin.hxx"
25 26 27
#include "../ArchivePlugin.hxx"
#include "../ArchiveFile.hxx"
#include "../ArchiveVisitor.hxx"
Max Kellermann's avatar
Max Kellermann committed
28
#include "input/InputStream.hxx"
29
#include "fs/Path.hxx"
30
#include "util/RuntimeError.hxx"
31
#include "util/StringCompare.hxx"
32
#include "util/UTF8.hxx"
33
#include "util/WritableBuffer.hxx"
34 35 36

#include <cdio/iso9660.h>

37
#include <array>
38
#include <utility>
39

40
#include <stdlib.h>
41 42
#include <string.h>

43 44
struct Iso9660 {
	iso9660_t *const iso;
45

46 47 48 49 50
	explicit Iso9660(Path path)
		:iso(iso9660_open(path.c_str())) {
		if (iso == nullptr)
			throw FormatRuntimeError("Failed to open ISO9660 file %s",
						 path.c_str());
51 52
	}

53 54
	~Iso9660() noexcept {
		iso9660_close(iso);
55 56
	}

57 58
	Iso9660(const Iso9660 &) = delete;
	Iso9660 &operator=(const Iso9660 &) = delete;
59

60 61 62
	long SeekRead(void *ptr, lsn_t start, long int i_size) const {
		return iso9660_iso_seek_read(iso, ptr, start, i_size);
	}
63 64 65 66 67 68
};

class Iso9660ArchiveFile final : public ArchiveFile {
	std::shared_ptr<Iso9660> iso;

public:
Max Kellermann's avatar
Max Kellermann committed
69
	explicit Iso9660ArchiveFile(std::shared_ptr<Iso9660> &&_iso)
70
		:iso(std::move(_iso)) {}
71

72 73 74 75
	/**
	 * @param capacity the path buffer size
	 */
	void Visit(char *path, size_t length, size_t capacity,
76
		   ArchiveVisitor &visitor);
77

78
	void Visit(ArchiveVisitor &visitor) override;
79

80
	InputStreamPtr OpenStream(const char *path,
81
				  Mutex &mutex) override;
82
};
83 84 85

/* archive open && listing routine */

86
inline void
87
Iso9660ArchiveFile::Visit(char *path, size_t length, size_t capacity,
88
			  ArchiveVisitor &visitor)
89
{
90
	auto *entlist = iso9660_ifs_readdir(iso->iso, path);
91 92 93 94
	if (!entlist) {
		return;
	}
	/* Iterate over the list of nodes that iso9660_ifs_readdir gives  */
95
	CdioListNode_t *entnode;
96
	_CDIO_LIST_FOREACH (entnode, entlist) {
97 98
		auto *statbuf = (iso9660_stat_t *)
			_cdio_list_node_data(entnode);
99
		const char *filename = statbuf->filename;
100 101 102
		if (StringIsEmpty(filename) ||
		    PathTraitsUTF8::IsSpecialFilename(filename))
			/* skip empty names (libcdio bug?) */
103
			/* skip special names like "." and ".." */
104
			continue;
105

106 107 108 109
		if (!ValidateUTF8(filename))
			/* ignore file names which are not valid UTF-8 */
			continue;

110
		size_t filename_length = strlen(filename);
111 112 113
		if (length + filename_length + 1 >= capacity)
			/* file name is too long */
			continue;
114

115 116
		memcpy(path + length, filename, filename_length + 1);
		size_t new_length = length + filename_length;
117

118
		if (iso9660_stat_s::_STAT_DIR == statbuf->type ) {
119
			memcpy(path + new_length, "/", 2);
120
			Visit(path, new_length + 1, capacity, visitor);
121 122
		} else {
			//remove leading /
123
			visitor.VisitArchiveEntry(path + 1);
124 125
		}
	}
126 127 128 129

#if LIBCDIO_VERSION_NUM >= 20000
	iso9660_filelist_free(entlist);
#else
130
	_cdio_list_free (entlist, true);
131
#endif
132 133
}

134
static std::unique_ptr<ArchiveFile>
135
iso9660_archive_open(Path pathname)
136
{
137
	return std::make_unique<Iso9660ArchiveFile>(std::make_shared<Iso9660>(pathname));
138 139
}

140 141
void
Iso9660ArchiveFile::Visit(ArchiveVisitor &visitor)
142
{
143
	char path[4096] = "/";
144
	Visit(path, 1, sizeof(path), visitor);
145 146 147 148
}

/* single archive handling */

149
class Iso9660InputStream final : public InputStream {
150
	std::shared_ptr<Iso9660> iso;
151

152
	const lsn_t lsn;
153

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
	/**
	 * libiso9660 can only read whole sectors at a time, and this
	 * buffer is used to store one whole sector and allow Read()
	 * to handle partial sector reads.
	 */
	class BlockBuffer {
		size_t position = 0, fill = 0;

		std::array<uint8_t, ISO_BLOCKSIZE> data;

	public:
		ConstBuffer<uint8_t> Read() const noexcept {
			assert(fill <= data.size());
			assert(position <= fill);

			return {&data[position], &data[fill]};
		}

		void Consume(size_t nbytes) noexcept {
			assert(nbytes <= Read().size);

			position += nbytes;
		}

		WritableBuffer<uint8_t> Write() noexcept {
			assert(Read().empty());

			return {data.data(), data.size()};
		}

		void Append(size_t nbytes) noexcept {
			assert(Read().empty());
			assert(nbytes <= data.size());

			fill = nbytes;
			position = 0;
		}
191 192 193 194

		void Clear() noexcept {
			position = fill = 0;
		}
195 196 197 198
	};

	BlockBuffer buffer;

199 200 201 202 203 204 205
	/**
	 * Skip this number of bytes of the first sector after filling
	 * the buffer next time.  This is used for seeking into the
	 * middle of a sector.
	 */
	size_t skip = 0;

206
public:
207
	Iso9660InputStream(std::shared_ptr<Iso9660> _iso,
208
			   const char *_uri,
209
			   Mutex &_mutex,
210
			   lsn_t _lsn, offset_type _size)
211
		:InputStream(_uri, _mutex),
212
		 iso(std::move(_iso)),
213 214 215
		 lsn(_lsn)
	{
		size = _size;
216
		seekable = true;
217
		SetReady();
218 219
	}

220
	/* virtual methods from InputStream */
221
	[[nodiscard]] bool IsEOF() const noexcept override;
222 223
	size_t Read(std::unique_lock<Mutex> &lock,
		    void *ptr, size_t size) override;
224 225

	void Seek(std::unique_lock<Mutex> &, offset_type new_offset) override {
226 227 228
		if (new_offset > size)
			throw std::runtime_error("Invalid seek offset");

229
		offset = new_offset;
230 231
		skip = new_offset % ISO_BLOCKSIZE;
		buffer.Clear();
232
	}
233 234
};

235
InputStreamPtr
236
Iso9660ArchiveFile::OpenStream(const char *pathname,
237
			       Mutex &mutex)
238
{
239
	auto statbuf = iso9660_ifs_stat_translate(iso->iso, pathname);
240 241 242
	if (statbuf == nullptr)
		throw FormatRuntimeError("not found in the ISO file: %s",
					 pathname);
243

244 245 246 247
	const lsn_t lsn = statbuf->lsn;
	const offset_type size = statbuf->size;
	free(statbuf);

248
	return std::make_unique<Iso9660InputStream>(iso, pathname, mutex,
249
						    lsn, size);
250 251
}

252
size_t
253 254
Iso9660InputStream::Read(std::unique_lock<Mutex> &,
			 void *ptr, size_t read_size)
255
{
256 257 258
	const offset_type remaining = size - offset;
	if (remaining == 0)
		return 0;
259

260 261
	if (offset_type(read_size) > remaining)
		read_size = remaining;
262

263
	auto r = buffer.Read();
264

265 266 267
	if (r.empty()) {
		/* the buffer is empty - read more data from the ISO file */

268
		assert((offset - skip) % ISO_BLOCKSIZE == 0);
269

270
		const ScopeUnlock unlock(mutex);
271

272
		const lsn_t read_lsn = lsn + offset / ISO_BLOCKSIZE;
273

274
		if (read_size >= ISO_BLOCKSIZE && skip == 0) {
275
			/* big read - read right into the caller's buffer */
276

277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
			auto nbytes = iso->SeekRead(ptr, read_lsn,
						    read_size / ISO_BLOCKSIZE);
			if (nbytes <= 0)
				throw std::runtime_error("Failed to read ISO9660 file");

			offset += nbytes;
			return nbytes;
		}

		/* fill the buffer */

		auto w = buffer.Write();
		auto nbytes = iso->SeekRead(w.data, read_lsn,
					    w.size / ISO_BLOCKSIZE);
		if (nbytes <= 0)
			throw std::runtime_error("Failed to read ISO9660 file");

		buffer.Append(nbytes);

		r = buffer.Read();
297 298 299 300 301 302 303 304 305 306

		if (skip > 0) {
			if (skip >= r.size)
				throw std::runtime_error("Premature end of ISO9660 track");

			buffer.Consume(skip);
			skip = 0;

			r = buffer.Read();
		}
307 308
	}

309
	assert(!r.empty());
310
	assert(skip == 0);
311 312 313 314 315 316

	size_t nbytes = std::min(read_size, r.size);
	memcpy(ptr, r.data, nbytes);
	buffer.Consume(nbytes);
	offset += nbytes;
	return nbytes;
317 318
}

319
bool
320
Iso9660InputStream::IsEOF() const noexcept
321
{
322
	return offset == size;
323 324 325 326
}

/* exported structures */

327
static constexpr const char * iso9660_archive_extensions[] = {
328
	"iso",
329
	nullptr
330 331
};

332
const ArchivePlugin iso9660_archive_plugin = {
333 334 335 336 337
	"iso",
	nullptr,
	nullptr,
	iso9660_archive_open,
	iso9660_archive_extensions,
338
};