1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "common/system.h"
24 #include "common/punycode.h"
25 #include "common/textconsole.h"
26 #include "backends/fs/abstract-fs.h"
27 #include "backends/fs/fs-factory.h"
28 
29 namespace Common {
30 
FSNode()31 FSNode::FSNode() {
32 }
33 
FSNode(AbstractFSNode * realNode)34 FSNode::FSNode(AbstractFSNode *realNode)
35 	: _realNode(realNode) {
36 }
37 
FSNode(const Path & p)38 FSNode::FSNode(const Path &p) {
39 	assert(g_system);
40 	FilesystemFactory *factory = g_system->getFilesystemFactory();
41 	AbstractFSNode *tmp = nullptr;
42 
43 	if (p.empty() || p == Path("."))
44 		tmp = factory->makeCurrentDirectoryFileNode();
45 	else
46 		tmp = factory->makeFileNodePath(p.toString());
47 	_realNode = SharedPtr<AbstractFSNode>(tmp);
48 }
49 
operator <(const FSNode & node) const50 bool FSNode::operator<(const FSNode& node) const {
51 	// Directories come before files, i.e., are "lower".
52 	if (isDirectory() != node.isDirectory())
53 		return isDirectory();
54 
55 	// If both nodes are of the same type (two files or two dirs),
56 	// then sort by name, ignoring case.
57 	return getName().compareToIgnoreCase(node.getName()) < 0;
58 }
59 
exists() const60 bool FSNode::exists() const {
61 	return _realNode && _realNode->exists();
62 }
63 
getChild(const String & n) const64 FSNode FSNode::getChild(const String &n) const {
65 	// If this node is invalid or not a directory, return an invalid node
66 	if (_realNode == nullptr || !_realNode->isDirectory())
67 		return FSNode();
68 
69 	AbstractFSNode *node = _realNode->getChild(n);
70 	return FSNode(node);
71 }
72 
getChildren(FSList & fslist,ListMode mode,bool hidden) const73 bool FSNode::getChildren(FSList &fslist, ListMode mode, bool hidden) const {
74 	if (!_realNode || !_realNode->isDirectory())
75 		return false;
76 
77 	AbstractFSList tmp;
78 
79 	if (!_realNode->getChildren(tmp, mode, hidden))
80 		return false;
81 
82 	fslist.clear();
83 	for (AbstractFSList::iterator i = tmp.begin(); i != tmp.end(); ++i) {
84 		fslist.push_back(FSNode(*i));
85 	}
86 
87 	return true;
88 }
89 
getDisplayName() const90 U32String FSNode::getDisplayName() const {
91 	assert(_realNode);
92 	return _realNode->getDisplayName();
93 }
94 
getName() const95 String FSNode::getName() const {
96 	assert(_realNode);
97 	// We transparently decode any punycode-named files
98 	return punycode_decodefilename(_realNode->getName());
99 }
100 
getParent() const101 FSNode FSNode::getParent() const {
102 	if (_realNode == nullptr)
103 		return *this;
104 
105 	AbstractFSNode *node = _realNode->getParent();
106 	if (node == nullptr) {
107 		return *this;
108 	} else {
109 		return FSNode(node);
110 	}
111 }
112 
getPath() const113 String FSNode::getPath() const {
114 	assert(_realNode);
115 	return _realNode->getPath();
116 }
117 
isDirectory() const118 bool FSNode::isDirectory() const {
119 	return _realNode && _realNode->isDirectory();
120 }
121 
isReadable() const122 bool FSNode::isReadable() const {
123 	return _realNode && _realNode->isReadable();
124 }
125 
isWritable() const126 bool FSNode::isWritable() const {
127 	return _realNode && _realNode->isWritable();
128 }
129 
createReadStream() const130 SeekableReadStream *FSNode::createReadStream() const {
131 	if (_realNode == nullptr)
132 		return nullptr;
133 
134 	if (!_realNode->exists()) {
135 		warning("FSNode::createReadStream: '%s' does not exist", getName().c_str());
136 		return nullptr;
137 	} else if (_realNode->isDirectory()) {
138 		warning("FSNode::createReadStream: '%s' is a directory", getName().c_str());
139 		return nullptr;
140 	}
141 
142 	return _realNode->createReadStream();
143 }
144 
createWriteStream() const145 SeekableWriteStream *FSNode::createWriteStream() const {
146 	if (_realNode == nullptr)
147 		return nullptr;
148 
149 	if (_realNode->isDirectory()) {
150 		warning("FSNode::createWriteStream: '%s' is a directory", getName().c_str());
151 		return nullptr;
152 	}
153 
154 	return _realNode->createWriteStream();
155 }
156 
createDirectory() const157 bool FSNode::createDirectory() const {
158 	if (_realNode == nullptr)
159 		return false;
160 
161 	if (_realNode->exists()) {
162 		if (_realNode->isDirectory()) {
163 			warning("FSNode::createDirectory: '%s' already exists", getName().c_str());
164 		} else {
165 			warning("FSNode::createDirectory: '%s' is a file", getName().c_str());
166 		}
167 		return false;
168 	}
169 
170 	return _realNode->createDirectory();
171 }
172 
FSDirectory(const FSNode & node,int depth,bool flat,bool ignoreClashes,bool includeDirectories)173 FSDirectory::FSDirectory(const FSNode &node, int depth, bool flat, bool ignoreClashes, bool includeDirectories)
174   : _node(node), _cached(false), _depth(depth), _flat(flat), _ignoreClashes(ignoreClashes),
175 	_includeDirectories(includeDirectories) {
176 }
177 
FSDirectory(const Path & prefix,const FSNode & node,int depth,bool flat,bool ignoreClashes,bool includeDirectories)178 FSDirectory::FSDirectory(const Path &prefix, const FSNode &node, int depth, bool flat,
179 						 bool ignoreClashes, bool includeDirectories)
180   : _node(node), _cached(false), _depth(depth), _flat(flat), _ignoreClashes(ignoreClashes),
181 	_includeDirectories(includeDirectories) {
182 
183 	setPrefix(prefix.rawString());
184 }
185 
FSDirectory(const Path & name,int depth,bool flat,bool ignoreClashes,bool includeDirectories)186 FSDirectory::FSDirectory(const Path &name, int depth, bool flat, bool ignoreClashes, bool includeDirectories)
187   : _node(name), _cached(false), _depth(depth), _flat(flat), _ignoreClashes(ignoreClashes),
188 	_includeDirectories(includeDirectories) {
189 }
190 
FSDirectory(const Path & prefix,const Path & name,int depth,bool flat,bool ignoreClashes,bool includeDirectories)191 FSDirectory::FSDirectory(const Path &prefix, const Path &name, int depth, bool flat,
192 						 bool ignoreClashes, bool includeDirectories)
193   : _node(name), _cached(false), _depth(depth), _flat(flat), _ignoreClashes(ignoreClashes),
194 	_includeDirectories(includeDirectories) {
195 
196 	setPrefix(prefix.rawString());
197 }
198 
~FSDirectory()199 FSDirectory::~FSDirectory() {
200 }
201 
setPrefix(const String & prefix)202 void FSDirectory::setPrefix(const String &prefix) {
203 	_prefix = prefix;
204 
205 	if (!_prefix.empty() && _prefix.lastChar() != DIR_SEPARATOR)
206 		_prefix += DIR_SEPARATOR;
207 }
208 
getFSNode() const209 FSNode FSDirectory::getFSNode() const {
210 	return _node;
211 }
212 
lookupCache(NodeCache & cache,const String & name) const213 FSNode *FSDirectory::lookupCache(NodeCache &cache, const String &name) const {
214 	// make caching as lazy as possible
215 	if (!name.empty()) {
216 		ensureCached();
217 
218 		if (cache.contains(name))
219 			return &cache[name];
220 	}
221 
222 	return nullptr;
223 }
224 
hasFile(const Path & path) const225 bool FSDirectory::hasFile(const Path &path) const {
226 	String name = path.rawString();
227 	if (name.empty() || !_node.isDirectory())
228 		return false;
229 
230 	FSNode *node = lookupCache(_fileCache, name);
231 	return node && node->exists();
232 }
233 
getMember(const Path & path) const234 const ArchiveMemberPtr FSDirectory::getMember(const Path &path) const {
235 	String name = path.rawString();
236 	if (name.empty() || !_node.isDirectory())
237 		return ArchiveMemberPtr();
238 
239 	FSNode *node = lookupCache(_fileCache, name);
240 
241 	if (!node || !node->exists()) {
242 		warning("FSDirectory::getMember: '%s' does not exist", Common::toPrintable(name).c_str());
243 		return ArchiveMemberPtr();
244 	} else if (node->isDirectory()) {
245 		warning("FSDirectory::getMember: '%s' is a directory", Common::toPrintable(name).c_str());
246 		return ArchiveMemberPtr();
247 	}
248 
249 	return ArchiveMemberPtr(new FSNode(*node));
250 }
251 
createReadStreamForMember(const Path & path) const252 SeekableReadStream *FSDirectory::createReadStreamForMember(const Path &path) const {
253 	String name = path.rawString();
254 	if (name.empty() || !_node.isDirectory())
255 		return nullptr;
256 
257 	FSNode *node = lookupCache(_fileCache, name);
258 	if (!node)
259 		return nullptr;
260 	SeekableReadStream *stream = node->createReadStream();
261 	if (!stream)
262 		warning("FSDirectory::createReadStreamForMember: Can't create stream for file '%s'", Common::toPrintable(name).c_str());
263 
264 	return stream;
265 }
266 
getSubDirectory(const Path & name,int depth,bool flat,bool ignoreClashes)267 FSDirectory *FSDirectory::getSubDirectory(const Path &name, int depth, bool flat, bool ignoreClashes) {
268 	return getSubDirectory(Path(), name, depth, flat, ignoreClashes);
269 }
270 
getSubDirectory(const Path & prefix,const Path & name,int depth,bool flat,bool ignoreClashes)271 FSDirectory *FSDirectory::getSubDirectory(const Path &prefix, const Path &name, int depth,
272 		bool flat, bool ignoreClashes) {
273 	String rawName = name.rawString();
274 	if (rawName.empty() || !_node.isDirectory())
275 		return nullptr;
276 
277 	FSNode *node = lookupCache(_subDirCache, rawName);
278 	if (!node)
279 		return nullptr;
280 
281 	return new FSDirectory(prefix, *node, depth, flat, ignoreClashes);
282 }
283 
cacheDirectoryRecursive(FSNode node,int depth,const Path & prefix) const284 void FSDirectory::cacheDirectoryRecursive(FSNode node, int depth, const Path& prefix) const {
285 	if (depth <= 0)
286 		return;
287 
288 	FSList list;
289 	node.getChildren(list, FSNode::kListAll);
290 
291 	FSList::iterator it = list.begin();
292 	for ( ; it != list.end(); ++it) {
293 		String name = prefix.rawString() + it->getName();
294 
295 		// don't touch name as it might be used for warning messages
296 		String lowercaseName = name;
297 		lowercaseName.toLowercase();
298 
299 		// since the hashmap is case insensitive, we need to check for clashes when caching
300 		if (it->isDirectory()) {
301 			if (!_flat && _subDirCache.contains(lowercaseName)) {
302 				// Always warn in this case as it's when there are 2 directories at the same place with different case
303 				// That means a problem in user installation as lookups are always done case insensitive
304 				warning("FSDirectory::cacheDirectory: name clash when building cache, ignoring sub-directory '%s'",
305 				        Common::toPrintable(name).c_str());
306 			} else {
307 				if (_subDirCache.contains(lowercaseName)) {
308 					if (!_ignoreClashes) {
309 						warning("FSDirectory::cacheDirectory: name clash when building subDirCache with subdirectory '%s'",
310 						        Common::toPrintable(name).c_str());
311 					}
312 				}
313 				cacheDirectoryRecursive(*it, depth - 1, _flat ? prefix : lowercaseName + DIR_SEPARATOR);
314 				_subDirCache[lowercaseName] = *it;
315 			}
316 		} else {
317 			if (_fileCache.contains(lowercaseName)) {
318 				if (!_ignoreClashes) {
319 					warning("FSDirectory::cacheDirectory: name clash when building cache, ignoring file '%s'",
320 					        Common::toPrintable(name).c_str());
321 				}
322 			} else {
323 				_fileCache[lowercaseName] = *it;
324 			}
325 		}
326 	}
327 
328 }
329 
ensureCached() const330 void FSDirectory::ensureCached() const  {
331 	if (_cached)
332 		return;
333 	cacheDirectoryRecursive(_node, _depth, _prefix);
334 	_cached = true;
335 }
336 
listMatchingMembers(ArchiveMemberList & list,const Path & pattern) const337 int FSDirectory::listMatchingMembers(ArchiveMemberList &list, const Path &pattern) const {
338 	if (!_node.isDirectory())
339 		return 0;
340 
341 	// Cache dir data
342 	ensureCached();
343 
344 	// need to match lowercase key, since all entries in our file cache are
345 	// stored as lowercase.
346 	String lowercasePattern(pattern.rawString());
347 	lowercasePattern.toLowercase();
348 
349 	// Prevent wildcards from matching the directory separator.
350 	const char wildcardExclusions[] = { DIR_SEPARATOR, '\0' };
351 
352 	int matches = 0;
353 	for (NodeCache::const_iterator it = _fileCache.begin(); it != _fileCache.end(); ++it) {
354 		if (it->_key.matchString(lowercasePattern, false, wildcardExclusions)) {
355 			list.push_back(ArchiveMemberPtr(new FSNode(it->_value)));
356 			matches++;
357 		}
358 	}
359 	if (_includeDirectories) {
360 		for (NodeCache::const_iterator it = _subDirCache.begin(); it != _subDirCache.end(); ++it) {
361 			if (it->_key.matchString(lowercasePattern, false, wildcardExclusions)) {
362 				list.push_back(ArchiveMemberPtr(new FSNode(it->_value)));
363 				matches++;
364 			}
365 		}
366 	}
367 
368 	return matches;
369 }
370 
listMembers(ArchiveMemberList & list) const371 int FSDirectory::listMembers(ArchiveMemberList &list) const {
372 	if (!_node.isDirectory())
373 		return 0;
374 
375 	// Cache dir data
376 	ensureCached();
377 
378 	int files = 0;
379 	for (NodeCache::const_iterator it = _fileCache.begin(); it != _fileCache.end(); ++it) {
380 		list.push_back(ArchiveMemberPtr(new FSNode(it->_value)));
381 		++files;
382 	}
383 
384 	if (_includeDirectories) {
385 		for (NodeCache::const_iterator it = _subDirCache.begin(); it != _subDirCache.end(); ++it) {
386 			list.push_back(ArchiveMemberPtr(new FSNode(it->_value)));
387 			++files;
388 		}
389 	}
390 
391 	return files;
392 }
393 
394 
395 } // End of namespace Common
396