fuse-zip-0.2.13/.hg_archival.txt0000644000000000000000000000022511477217250016370 0ustar rootroot00000000000000repo: 38472f434ab14d41686baaa73679d7fe4773f0f2 node: 3bd5d9cfa78b573ab7ef042255e7a279463c32f6 branch: default latesttag: 0.2.13 latesttagdistance: 1 fuse-zip-0.2.13/.hgignore0000644000000000000000000000035711477217250015113 0ustar rootroot00000000000000syntax: glob # ignore vim swapfiles .*.swp # ignore source archives fuse-zip*.tar.gz # ignore generated files *.o fuse-zip fuse-zip.1.gz lib/*.a # tests tests/whitebox/*.x # other related projects (needed for performance tests) others fuse-zip-0.2.13/.hgtags0000644000000000000000000000144311477217250014563 0ustar rootroot000000000000000ac39c893b9516994afab8e0524765a1cd54739b 0.2.0 0d5865a7fb53959088bfd4b1edc028de8c64de53 0.2.6 1e34f469bf9d39a34cf2dcd74be3762f75787cf9 0.2.5 581b06b7cee2b7691125e2907683f4e834e90301 0.1.0 5e0f0c7ae8049eb675386622c53d5f0b487512a6 0.2.3 9513e1d44bf99244b9a037ea30cc01fbe8ea5856 0.2.1 9ab3241a6822263558b2db2a9e26c0f4d2ca524f 0.2.4 fd88cde887ac7405b81f2c9a73a61ed6af2fc99a 0.2.2 80ad596796392ffedd0281c822d8f4bfd537134f 0.2.7 ab463806418aff3429772941253e0f63503b58a2 0.2.8 5edac8c7e535a992741658de1ba1fcb6058e1305 0.2.9 5edac8c7e535a992741658de1ba1fcb6058e1305 0.2.9 95321b27fefb951c3026beddbe5820ef4c3ac4b1 0.2.9 19110f42d5f6ed094bbdeead843a969845b18907 0.2.10 256f2f59af7b8e099479157507852cfd00bbf085 0.2.11 e941c5cc7a9c1eb115fd1f4db7907dddc2cbc8dc 0.2.12 3dd1661895752bb47dc11b37f0c8afa7fff9e272 0.2.13 fuse-zip-0.2.13/INSTALL0000644000000000000000000000070611477217250014337 0ustar rootroot00000000000000You need the following libraries: libfuse >= 2.7 http://fuse.sourceforge.net libzip >= 0.8 http://www.nih.at/libzip/ The following tools are required: C++ compiler g++ 4.2.3 or other modern C++ compiler pkg-config http://pkg-config.freedesktop.org/ GNU make http://www.gnu.org/software/make/ To build fuse-zip do the following: $ make release To install do: # make install as root or use sudo. To uninstall: # make uninstall fuse-zip-0.2.13/LICENSE0000644000000000000000000001672711477217250014325 0ustar rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. fuse-zip-0.2.13/Makefile0000644000000000000000000000326211477217250014746 0ustar rootroot00000000000000DEST=fuse-zip LIBS=$(shell pkg-config fuse --libs) $(shell pkg-config libzip --libs) -Llib -lfusezip LIB=lib/libfusezip.a CXXFLAGS=-g -O2 -Wall -Wextra RELEASE_CXXFLAGS=-O2 -Wall -Wextra FUSEFLAGS=$(shell pkg-config fuse --cflags) SOURCES=main.cpp OBJECTS=$(SOURCES:.cpp=.o) MANSRC=fuse-zip.1 MAN=fuse-zip.1.gz CLEANFILES=$(OBJECTS) $(MAN) DOCFILES=README changelog INSTALLPREFIX=/usr all: $(DEST) doc: $(MAN) doc-clean: man-clean $(DEST): $(OBJECTS) $(LIB) $(CXX) $(LDFLAGS) $(OBJECTS) $(LIBS) \ -o $@ # main.cpp must be compiled separately with FUSEFLAGS main.o: main.cpp $(CXX) -c $(CXXFLAGS) $(FUSEFLAGS) $< \ -Ilib \ -o $@ $(LIB): make -C lib lib-clean: make -C lib clean distclean: clean doc-clean rm -f $(DEST) clean: lib-clean all-clean test-clean tarball-clean all-clean: rm -f $(CLEANFILES) $(MAN): $(MANSRC) gzip -c9 $< > $@ man-clean: rm -f $(MANSRC).gz install: all doc mkdir -p "$(INSTALLPREFIX)/bin" install -m 755 -s "$(DEST)" "$(INSTALLPREFIX)/bin" mkdir -p "$(INSTALLPREFIX)/share/doc/$(DEST)" cp $(DOCFILES) "$(INSTALLPREFIX)/share/doc/$(DEST)" mkdir -p "$(INSTALLPREFIX)/share/man/man1" cp $(MAN) "$(INSTALLPREFIX)/share/man/man1" uninstall: rm "$(INSTALLPREFIX)/bin/$(DEST)" rm -r "$(INSTALLPREFIX)/share/doc/$(DEST)" rm "$(INSTALLPREFIX)/share/man/man1/$(MAN)" tarball: ./makeArchives.sh tarball-clean: rm -f fuse-zip-*.tar.gz fuse-zip-tests-*.tar.gz release: make CXXFLAGS="$(RELEASE_CXXFLAGS)" all doc test: $(DEST) make -C tests test-clean: make -C tests clean valgrind: make -C tests valgrind .PHONY: all release doc clean all-clean lib-clean doc-clean test-clean tarball-clean distclean install uninstall tarball test valgrind fuse-zip-0.2.13/README0000644000000000000000000000426011477217250014165 0ustar rootroot00000000000000= ABOUT = fuse-zip is a FUSE file system to navigate, extract, create and modify ZIP archives based in libzip implemented in C++. With fuse-zip you really can work with ZIP archives as real directories. Unlike KIO or Gnome VFS, it can be used in any application without modifications. Unlike other FUSE filesystems, _only_ fuse-zip provides write support to ZIP archives. Also, fuse-zip is faster that all known implementations on large archives with many files. You can download fuse-zip at http://code.google.com/p/fuse-zip = AUTHOR = Alexander Galanin * E-mail: al@galanin.nnov.ru * XMPP: gaa@jabber.ru * Homepage: http://galanin.nnov.ru/~al/ = LICENSE = fuse-zip are licensed under GNU LGPL v3. = USAGE = {{{ $ mkdir /tmp/zipArchive $ fuse-zip foobar.zip /tmp/zipArchive (do something with the mounted file system) $ fusermount -u /tmp/zipArchive }}} If ZIP file does not exists, it will be created after filesystem unmounting. Be patient. Wait for fuse-zip process finish after unmounting especially on a big archives. If you want to specify character set conversion for file names in archive, use the following fusermount options: -omodules=iconv,from_code=$charset1,to_code=$charset2 Those Russian who uses archives from the "other OS" should use CP866 as 'charset1' and locale charset as 'charset2'. See FUSE documentation for details. Look at /var/log/user.log in case of any errors. = PERFORMANCE = On a small archives fuse-zip have the same performance with commonly used virtual filesystems like KIO, Gnome GVFS, mc vfs, unpackfs, avfs, fuse-j-zip. But on large archives with many file (like zipped Linux kernel sources) fuse-zip have the greatest speed. You can download test suite from the web-site and make sure that it is true. See PerformancePage for details. = HINTS = # Added/changed files resides into memory until you unmount file system. # Adding/unpacking very big files(more than one half of available memory) may cause your system swapping. It is a good idea to use zip/unzip in that case :) # After adding/modifying files in archive it will be repacked at filesystem unmount. Hence, your file system must have enough space to keep tempopary files. fuse-zip-0.2.13/TODO0000644000000000000000000000103411477217250013771 0ustar rootroot00000000000000* whitebox test suite * more unified exceptions and error cases handling * replace vector with (s)list in BigBuffer * rewrite files tree: keep only short path information to simplify rename(), use hash_map (unordered_map) instead of map * log zip file errors to syslog * truncate() without open-close file * incapsulate call for zip_* into FileNode * speed up position determination using bitwise operations * iterate over chunks by iterator instead of indexing * write memory cache to /tmp on fsync(). May be write compressed stream fuse-zip-0.2.13/changelog0000644000000000000000000000642311477217250015162 0ustar rootroot000000000000002010-12-06 Alexander Galanin * Released 0.2.13: - Fixed issue #27: Android APK Support - Fixed issue #28: doesn't honor the -r flag for some zip files 2010-02-07 Alexander Galanin * Released 0.2.12: - Fixed problem with lost new file after truncate(). - Fixed problems with rename(): Fixed various rename() problems: lost path for subdirectory entries, duplicates of moved directories in a hierarchy, invalid key in map after rename. - Fixed unitialized values in read() call. - Fixed memory leaks: buffer allocated for file content not freeed in NEW state, incorrect buffer size in truncate(). - Fixed non-fatal memory leaks: FUSE options not freeed after use, memory leak in help/version mode, internal data structures not freeed if FUSE setup failed. - More correct corrupted files handling. - More correct memory insufficiency errors handling. 2010-01-26 Alexander Galanin * Released 0.2.11: - Fixed issue #25: does not compile with libfuse <= 2.8 2010-01-09 Alexander Galanin * Released 0.2.10: - Fixed issue #14: added '-r' option description. - Added note about converting file names inside archive (for Russians who uses 'another OS') 2010-01-08 Alexander Galanin * Released 0.2.9: - Fixed issue #22, now command-line options are correctly processed 2009-11-18 Alexander Galanin * Released 0.2.8: - Fixed issue #20: incorrect directory size reported 2008-12-06 Alexander Galanin * Released 0.2.7: - Fixed segfault if user tried to re-open corrupted file from an invalid archive. 2008-09-29 Alexander Galanin * Released 0.2.6: - Fixed: Compilation error on FreeBSD/Mac OS X with FUSE 2.7 2008-08-24 Alexander Galanin * Released 0.2.5: - Fixed: Archives containing files whose parent directory does not present in archive, reported as broken. 2008-08-09 Alexander Galanin * Released 0.2.4: - fixed wrong directory size on 32-bit Linux machines 2008-06-26 Alexander Galanin * Released 0.2.3: - fixed problem with time and size of new files - free space on file system now equal to free space in archive dir - added missing includes - removed GNU-specific commands in Makefile 2008-06-16 Alexander Galanin * Released 0.2.2: - re-licensed under LGPLv3+ - fixed problem with file modification time - fixed problems with compilation on non-Linux systems 2008-06-14 Alexander Galanin * Released 0.2.1: - fixed bug with file size on 32-bit systems - fixed compilation problems on non-GNU platforms - fixed compilation problems on GCC 4.3+ - added stubs for *xattr() system calls - more checks for error cases 2008-06-11 Alexander Galanin * Released 0.2.0: - implemented all functions to mount archive read-write - big internal refactoring - added performance tests - fixed problem with simultaneous file access - fixed problem with one-character file names * Known limitations: - archives with items like '../../somefile' recognited as broken 2008-06-04 Alexander Galanin * Initial release 0.1.0: - implemented all base functions for read-only archive support fuse-zip-0.2.13/config.h0000644000000000000000000000320111477217250014715 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef CONFIG_H #define CONFIG_H #define FUSE_USE_VERSION 27 #define PROGRAM "fuse-zip" #define VERSION "0.2.13" #endif fuse-zip-0.2.13/fuse-zip.10000644000000000000000000000414011477217250015126 0ustar rootroot00000000000000.\" '\" t .\" ** The above line should force tbl to be a preprocessor ** .\" Man page for fuse-zip .TH "fuse-zip" "1" "January 2010" "FUSE filesystem to read and modify ZIP archives" "FUSE filesystem to read and modify ZIP archives" .SH "NAME" fuse\-zip \- a FUSE filesystem for zip archives with write support .SH "SYNOPSIS" .\" The general command line .B fuse\-zip .RI [\| options \|] zip\-file mount\-point .SH "OPTIONS" .TP \fB-h\fP print help .TP \fB-V\fP print version .TP \fB-r\fP open archive in read\-only mode .TP \fB-o opt[,opt...]\fP mount options .TP \fB-f\fP don't detach from terminal .TP \fB-d\fP turn on debugging, also implies \-f .PP If you want to specify character set conversion for file names in archive, use the following fusermount options: \-omodules=iconv,from_code=$charset1,to_code=$charset2 See FUSE documentation for details. .SH "DESCRIPTION" .B fuse\-zip is a fuse filesystem, that enables any program to work with a ZIP archive as though it is a plain directory. Unlike KIO or Gnome VFS, it can be used in any application without modifications. Unlike other FUSE filesystems, only fuse\-zip provides write support to ZIP archives. Also, fuse\-zip is faster that all known implementations on large archives with many files. .SH "USAGE" General usage would look like this .TS tab (@); l l. 1@mkdir\ /tmp/zipArchive 2@fuse\-zip foobar.zip /tmp/zipArchive 3@(do something with the mounted file system) 4@fusermount \-u /tmp/zipArchive .TE .PP Be patient. Wait for fuse-zip process finish after unmounting, especially on a big archives. .SH "FILES" .TP .if !'po4a'hide' .I /var/log/user.log see this file in case any errors occur .SH "SEE ALSO" .BR fusermount (1). .SH "LICENSE" . This is Free Software; this software is licensed under the LGPL version 3, as published by the Free Software Foundation, or later. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. . .SH "AUTHORS" . Alexander Galanin http://galanin.nnov.ru/~al .br . This manual page was originally written by Kirill Zaitsev . Updated by Alexander Galanin. fuse-zip-0.2.13/lib/Makefile0000644000000000000000000000125511477217250015514 0ustar rootroot00000000000000DEST=libfusezip.a LIBS=$(shell pkg-config fuse --libs) $(shell pkg-config libzip --libs) CXXFLAGS=-g -O2 -Wall -Wextra RELEASE_CXXFLAGS=-O2 -Wall -Wextra FUSEFLAGS=$(shell pkg-config fuse --cflags) ZIPFLAGS=$(shell pkg-config libzip --cflags) SOURCES=$(wildcard *.cpp) OBJECTS=$(SOURCES:.cpp=.o) CLEANFILES=$(OBJECTS) $(DEST) all: $(DEST) release: make CXXFLAGS="$(RELEASE_CXXFLAGS)" all $(DEST): $(OBJECTS) $(AR) -cq $@ $(OBJECTS) # fuse-zip.cpp must be compiled separately with FUSEFLAGS fuse-zip.o: fuse-zip.cpp $(CXX) -c $(CXXFLAGS) $(FUSEFLAGS) $(ZIPFLAGS) $< -o $@ .cpp.o: $(CXX) -c $(CXXFLAGS) $(ZIPFLAGS) $< -o $@ clean: rm -f $(DEST) $(OBJECTS) .PHONY: all clean fuse-zip-0.2.13/lib/bigBuffer.cpp0000644000000000000000000002042611477217250016454 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #include #include #include #include "bigBuffer.h" /** * Class that keep chunk of file data. */ class BigBuffer::ChunkWrapper { private: /** * Pointer that keeps data for chunk. Can be NULL. */ char *m_ptr; public: /** * By default internal buffer is NULL, so this can be used for creating * sparse files. */ ChunkWrapper(): m_ptr(NULL) { } /** * Take ownership on internal pointer from 'other' object. */ ChunkWrapper(const ChunkWrapper &other) { m_ptr = other.m_ptr; const_cast(&other)->m_ptr = NULL; } /** * Free pointer if allocated. */ ~ChunkWrapper() { if (m_ptr != NULL) { free(m_ptr); } } /** * Take ownership on internal pointer from 'other' object. */ ChunkWrapper &operator=(const ChunkWrapper &other) { if (&other != this) { m_ptr = other.m_ptr; const_cast(&other)->m_ptr = NULL; } return *this; } /** * Return pointer to internal storage and initialize it if needed. * @throws * std::bad_alloc If memory can not be allocated */ char *ptr(bool init = false) { if (init && m_ptr == NULL) { m_ptr = (char *)malloc(chunkSize); if (m_ptr == NULL) { throw std::bad_alloc(); } } return m_ptr; } /** * Fill 'dest' with internal buffer content. * If m_ptr is NULL, destination bytes is zeroed. * * @param dest Destination buffer. * @param offset Offset in internal buffer to start reading from. * @param count Number of bytes to be read. * * @return Number of bytes actually read. It can differ with 'count' * if offset+count>chunkSize. */ size_t read(char *dest, offset_t offset, size_t count) const { if (offset + count > chunkSize) { count = chunkSize - offset; } if (m_ptr != NULL) { memcpy(dest, m_ptr + offset, count); } else { memset(dest, 0, count); } return count; } /** * Fill internal buffer with bytes from 'src'. * If m_ptr is NULL, memory for buffer is malloc()-ed and then head of * allocated space is zeroed. After that byte copying is performed. * * @param src Source buffer. * @param offset Offset in internal buffer to start writting from. * @param count Number of bytes to be written. * * @return Number of bytes actually written. It can differ with * 'count' if offset+count>chunkSize. * @throws * std::bad_alloc If there are no memory for buffer */ size_t write(const char *src, offset_t offset, size_t count) { if (offset + count > chunkSize) { count = chunkSize - offset; } if (m_ptr == NULL) { m_ptr = (char *)malloc(chunkSize); if (m_ptr == NULL) { throw std::bad_alloc(); } if (offset > 0) { memset(m_ptr, 0, offset); } } memcpy(m_ptr + offset, src, count); return count; } /** * Clear tail of internal buffer with zeroes starting from 'offset'. */ void clearTail(offset_t offset) { if (m_ptr != NULL && offset < chunkSize) { memset(m_ptr + offset, 0, chunkSize - offset); } } }; BigBuffer::BigBuffer(): len(0) { } BigBuffer::BigBuffer(struct zip *z, int nodeId, ssize_t length): len(length) { struct zip_file *zf = zip_fopen_index(z, nodeId, 0); if (zf == NULL) { throw std::exception(); } chunks.resize(chunksCount(length), ChunkWrapper()); unsigned int chunk = 0; ssize_t nr; while (length > 0) { nr = zip_fread(zf, chunks[chunk].ptr(true), chunkSize); if (nr < 0) { zip_fclose(zf); throw std::exception(); } ++chunk; length -= nr; } if (zip_fclose(zf)) { throw std::exception(); } } BigBuffer::~BigBuffer() { } int BigBuffer::read(char *buf, size_t size, offset_t offset) const { if (offset > len) { return 0; } int chunk = chunkNumber(offset); int pos = chunkOffset(offset); if (size > unsigned(len - offset)) { size = len - offset; } int nread = size; while (size > 0) { size_t r = chunks[chunk].read(buf, pos, size); size -= r; buf += r; ++chunk; pos = 0; } return nread; } int BigBuffer::write(const char *buf, size_t size, offset_t offset) { int chunk = chunkNumber(offset); int pos = chunkOffset(offset); int nwritten = size; if (offset > len) { if (len > 0) { chunks[chunkNumber(len)].clearTail(chunkOffset(len)); } len = size + offset; } else if (size > unsigned(len - offset)) { len = size + offset; } chunks.resize(chunksCount(len)); while (size > 0) { size_t w = chunks[chunk].write(buf, pos, size); size -= w; buf += w; ++ chunk; pos = 0; } return nwritten; } void BigBuffer::truncate(offset_t offset) { chunks.resize(chunksCount(offset)); if (offset > len && len > 0) { // Fill end of last non-empty chunk with zeroes chunks[chunkNumber(len)].clearTail(chunkOffset(len)); } len = offset; } ssize_t BigBuffer::zipUserFunctionCallback(void *state, void *data, size_t len, enum zip_source_cmd cmd) { CallBackStruct *b = (CallBackStruct*)state; switch (cmd) { case ZIP_SOURCE_OPEN: { b->pos = 0; return 0; } case ZIP_SOURCE_READ: { int r = b->buf->read((char*)data, len, b->pos); b->pos += r; return r; } case ZIP_SOURCE_STAT: { struct zip_stat *st = (struct zip_stat*)data; zip_stat_init(st); st->size = b->buf->len; st->mtime = b->mtime; return sizeof(struct zip_stat); } case ZIP_SOURCE_FREE: { delete b; return 0; } default: { return 0; } } } int BigBuffer::saveToZip(time_t mtime, struct zip *z, const char *fname, bool newFile, int index) { struct zip_source *s; struct CallBackStruct *cbs = new CallBackStruct(); cbs->buf = this; cbs->mtime = mtime; if ((s=zip_source_function(z, zipUserFunctionCallback, cbs)) == NULL) { delete cbs; return -ENOMEM; } if ((newFile && zip_add(z, fname, s) < 0) || (!newFile && zip_replace(z, index, s) < 0)) { delete cbs; zip_source_free(s); return -ENOMEM; } return 0; } fuse-zip-0.2.13/lib/bigBuffer.h0000644000000000000000000001224511477217250016121 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef BIG_BUFFER_H #define BIG_BUFFER_H #include #include #include #include "types.h" class BigBuffer { private: //TODO: use >> and << static const unsigned int chunkSize = 4*1024; //4 Kilobytes class ChunkWrapper; typedef std::vector chunks_t; struct CallBackStruct { size_t pos; const BigBuffer *buf; time_t mtime; }; chunks_t chunks; /** * Callback for zip_source_function. * ZIP_SOURCE_CLOSE is not needed to be handled, ZIP_SOURCE_ERROR is * never called because read() always successfull. * See zip_source_function(3) for details. */ static ssize_t zipUserFunctionCallback(void *state, void *data, size_t len, enum zip_source_cmd cmd); /** * Return number of chunks needed to keep 'offset' bytes. */ inline static unsigned int chunksCount(offset_t offset) { return (offset + chunkSize - 1) / chunkSize; } /** * Return number of chunk where 'offset'-th byte is located. */ inline static unsigned int chunkNumber(offset_t offset) { return offset / chunkSize; } /** * Return offset inside chunk to 'offset'-th byte. */ inline static int chunkOffset(offset_t offset) { return offset % chunkSize; } public: offset_t len; /** * Create new file buffer without mapping to file in a zip archive */ BigBuffer(); /** * Read file data from file inside zip archive * * @param z Zip file * @param nodeId Node index inside zip file * @param length File length * @throws * std::exception On file read error * std::bad_alloc On memory insufficiency */ BigBuffer(struct zip *z, int nodeId, ssize_t length); ~BigBuffer(); /** * Dispatch read requests to chunks of a file and write result to * resulting buffer. * Reading after end of file is not allowed, so 'size' is decreased to * fit file boundaries. * * @param buf destination buffer * @param size requested bytes count * @param offset offset to start reading from * @return always 0 */ int read(char *buf, size_t size, offset_t offset) const; /** * Dispatch write request to chunks of a file and grow 'chunks' vector if * necessary. * If 'offset' is after file end, tail of last chunk cleared before growing. * * @param buf Source buffer * @param size Number of bytes to be written * @param offset Offset in file to start writing from * @return number of bytes written * @throws * std::bad_alloc If there are no memory for buffer */ int write(const char *buf, size_t size, offset_t offset); /** * Create (or replace) file element in zip file. Class instance should * not be destroyed until zip_close() is called. * * @param mtime File modification time * @param z ZIP archive structure * @param fname File name * @param newFile Is file not yet created? * @param index File index in ZIP archive * @return * 0 If successfull * -ENOMEM If there are no memory */ int saveToZip(time_t mtime, struct zip *z, const char *fname, bool newFile, int index); /** * Truncate buffer at position offset. * 1. Free chunks after offset * 2. Resize chunks vector to a new size * 3. Fill data block that made readable by resize with zeroes * * @throws * std::bad_alloc If insufficient memory available */ void truncate(offset_t offset); }; #endif fuse-zip-0.2.13/lib/fileNode.cpp0000644000000000000000000001614111477217250016305 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include "fileNode.h" #include "fuseZipData.h" const int FileNode::ROOT_NODE_INDEX = -1; const int FileNode::NEW_NODE_INDEX = -2; FileNode::FileNode(FuseZipData *_data, const char *fname, int id): data(_data) { this->id = id; this->is_dir = false; this->open_count = 0; if (id == NEW_NODE_INDEX) { state = NEW; buffer = new BigBuffer(); if (!buffer) { throw std::bad_alloc(); } zip_stat_init(&stat); stat.mtime = time(NULL); stat.size = 0; } else { state = CLOSED; if (id != ROOT_NODE_INDEX) { zip_stat_index(data->m_zip, this->id, 0, &stat); } else { zip_stat_init(&stat); stat.mtime = time(NULL); stat.size = 0; } } char *t = strdup(fname); if (t == NULL) { throw std::bad_alloc(); } parse_name(t); try { attach(); } catch (...) { free(full_name); throw; } } FileNode::~FileNode() { free(full_name); if (state == OPENED || state == CHANGED || state == NEW) { delete buffer; } } void FileNode::parse_name(char *fname) { this->full_name = fname; if (*fname == '\0') { // in case of root directory of a virtual filesystem this->name = this->full_name; this->is_dir = true; } else { char *lsl = full_name; while (*lsl++) {} lsl--; while (lsl > full_name && *lsl != '/') { lsl--; } // If the last symbol in file name is '/' then it is a directory if (*lsl == '/' && *(lsl+1) == '\0') { // It will produce two \0s at the end of file name. I think that it is not a problem *lsl = '\0'; this->is_dir = true; while (lsl > full_name && *lsl != '/') { lsl--; } } // Setting short name of node if (*lsl == '/') { lsl++; } this->name = lsl; } } void FileNode::attach() { filemap_t::const_iterator other_iter = this->data->files.find(this->full_name); // If Node with the same name already exists... if (other_iter != this->data->files.end()) { FileNode *other = other_iter->second; if (other->id != FileNode::NEW_NODE_INDEX) { throw std::exception(); } // ... update existing node information other->id = this->id; other->stat = this->stat; other->state = this->state; throw AlreadyExists(); } if (*this->full_name != '\0') { // Adding new child to parent node. For items without '/' in fname it will be root_node. char *lsl = this->name; if (lsl > this->full_name) { lsl--; } char c = *lsl; *lsl = '\0'; // Searching for parent node... filemap_t::const_iterator parent_iter = data->files.find(this->full_name); if (parent_iter == data->files.end()) { // Relative paths are not supported if (strcmp(this->full_name, ".") == 0 || strcmp(this->full_name, "..") == 0) { throw std::exception(); } FileNode *p = new FileNode(data, this->full_name); p->is_dir = true; this->parent = p; } else { this->parent = parent_iter->second; } this->parent->childs.push_back(this); *lsl = c; } this->data->files[this->full_name] = this; } void FileNode::detach() { data->files.erase(full_name); parent->childs.remove(this); } void FileNode::rename(char *fname) { detach(); free(full_name); parse_name(fname); attach(); } void FileNode::rename_wo_reparenting(char *new_name) { data->files.erase(full_name); free(full_name); parse_name(new_name); data->files[new_name] = this; } int FileNode::open() { if (state == NEW) { return 0; } if (state == OPENED) { if (open_count == INT_MAX) { return -EMFILE; } else { ++open_count; } } if (state == CLOSED) { open_count = 1; try { buffer = new BigBuffer(data->m_zip, id, stat.size); state = OPENED; } catch (std::bad_alloc) { return -ENOMEM; } catch (std::exception) { return -EIO; } } return 0; } int FileNode::read(char *buf, size_t size, offset_t offset) const { return buffer->read(buf, size, offset); } int FileNode::write(const char *buf, size_t size, offset_t offset) { if (state == OPENED) { state = CHANGED; } return buffer->write(buf, size, offset); } int FileNode::close() { stat.size = buffer->len; if (state == OPENED && --open_count == 0) { delete buffer; state = CLOSED; } if (state == CHANGED) { stat.mtime = time(NULL); } return 0; } int FileNode::save() { return buffer->saveToZip(stat.mtime, data->m_zip, full_name, state == NEW, id); } int FileNode::truncate(offset_t offset) { if (state != CLOSED) { if (state != NEW) { state = CHANGED; } try { buffer->truncate(offset); return 0; } catch (const std::bad_alloc &) { return EIO; } } else { return EBADF; } } offset_t FileNode::size() const { if (state == NEW || state == OPENED || state == CHANGED) { return buffer->len; } else { return stat.size; } } fuse-zip-0.2.13/lib/fileNode.h0000644000000000000000000000641311477217250015753 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef FILE_NODE_H #define FILE_NODE_H #include #include "types.h" #include "bigBuffer.h" class FileNode { private: enum nodeState { CLOSED, OPENED, CHANGED, NEW }; BigBuffer *buffer; FuseZipData *data; int open_count; nodeState state; void parse_name(char *fname); void attach(); public: static const int ROOT_NODE_INDEX, NEW_NODE_INDEX; FileNode(FuseZipData *_data, const char *fname, int id = NEW_NODE_INDEX); ~FileNode(); void detach(); void rename(char *fname); /** * Rename file without reparenting. * * 1. Remove file item from tree * 2. Free ols file name string * 3. Parse new name * 4. Create link to new name in tree */ void rename_wo_reparenting(char *new_name); int open(); int read(char *buf, size_t size, offset_t offset) const; int write(const char *buf, size_t size, offset_t offset); int close(); /** * Invoke zip_add() or zip_replace() for file to save it. * Should be called only if item is needed to ba saved into zip file. * * @return 0 if success, != 0 on error */ int save(); /** * Truncate file. * * @return * 0 If successful * EBADF If file is currently closed * EIO If insufficient memory available (because ENOMEM not * listed in truncate() error codes) */ int truncate(offset_t offset); inline bool isChanged() const { return state == CHANGED || state == NEW; } offset_t size() const; char *name, *full_name; bool is_dir; int id; nodelist_t childs; FileNode *parent; struct zip_stat stat; class AlreadyExists { }; }; #endif fuse-zip-0.2.13/lib/fuse-zip.cpp0000644000000000000000000003104111477217250016316 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #define STANDARD_BLOCK_SIZE (512) #define ERROR_STR_BUF_LEN 0x100 #include "../config.h" #include #include #include #include #include #include #include #include #include #include #include #include "fuse-zip.h" #include "types.h" #include "fileNode.h" #include "fuseZipData.h" #include "libZipWrapper.h" using namespace std; //TODO: Move printf-s out this function FuseZipData *initFuseZip(const char *program, const char *fileName) { FuseZipData *data = NULL; int err; struct zip *zip_file; if ((zip_file = zip_open(fileName, ZIP_CREATE, &err)) == NULL) { char err_str[ERROR_STR_BUF_LEN]; zip_error_to_str(err_str, ERROR_STR_BUF_LEN, err, errno); fprintf(stderr, "%s: cannot open zip archive %s: %s\n", program, fileName, err_str); return data; } try { // current working directory char *cwd = (char*)malloc(PATH_MAX + 1); if (getcwd(cwd, PATH_MAX) == NULL) { perror(NULL); return data; } data = new FuseZipData(fileName, zip_file, cwd); if (data == NULL) { throw std::bad_alloc(); } } catch (std::bad_alloc) { fprintf(stderr, "%s: no enough memory\n", program); } catch (std::exception) { fprintf(stderr, "%s: ZIP file corrupted\n", program); } return data; } void *fusezip_init(struct fuse_conn_info *conn) { (void) conn; FuseZipData *data = (FuseZipData*)fuse_get_context()->private_data; syslog(LOG_INFO, "Mounting file system on %s (cwd=%s)", data->m_archiveName, data->m_cwd); return data; } void fusezip_destroy(void *data) { FuseZipData *d = (FuseZipData*)data; // Saving changed data for (filemap_t::const_iterator i = d->files.begin(); i != d->files.end(); ++i) { if (i->second->isChanged() && !i->second->is_dir) { int res = i->second->save(); if (res != 0) { syslog(LOG_ERR, "Error while saving file %s in ZIP archive: %d", i->second->full_name, res); } } } delete d; syslog(LOG_INFO, "File system unmounted"); } inline FuseZipData *get_data() { return (FuseZipData*)fuse_get_context()->private_data; } FileNode *get_file_node(const char *fname) { FuseZipData *data = get_data(); filemap_t::iterator i = data->files.find(fname); if (i == data->files.end()) { return NULL; } else { return i->second; } } inline struct zip *get_zip() { return get_data()->m_zip; } int fusezip_getattr(const char *path, struct stat *stbuf) { memset(stbuf, 0, sizeof(struct stat)); if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } struct zip_stat_64 zstat; zip_stat_assign_64_to_default(&zstat, &node->stat); if (node->is_dir) { stbuf->st_mode = S_IFDIR | 0755; stbuf->st_nlink = 2 + node->childs.size(); } else { stbuf->st_mode = S_IFREG | 0644; stbuf->st_nlink = 1; } stbuf->st_blksize = STANDARD_BLOCK_SIZE; stbuf->st_ino = node->id; stbuf->st_blocks = (node->size() + STANDARD_BLOCK_SIZE - 1) / STANDARD_BLOCK_SIZE; stbuf->st_size = node->size(); stbuf->st_atime = stbuf->st_mtime = stbuf->st_ctime = zstat.mtime; stbuf->st_uid = geteuid(); stbuf->st_gid = getegid(); return 0; } int fusezip_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) { (void) offset; (void) fi; if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } filler(buf, ".", NULL, 0); filler(buf, "..", NULL, 0); for (nodelist_t::const_iterator i = node->childs.begin(); i != node->childs.end(); ++i) { filler(buf, (*i)->name, NULL, 0); } return 0; } int fusezip_statfs(const char *path, struct statvfs *buf) { (void) path; // Getting amount of free space in directory with archive struct statvfs st; int err; if ((err = statvfs(get_data()->m_cwd,&st)) != 0) { return -err; } buf->f_bavail = buf->f_bfree = st.f_frsize * st.f_bavail; buf->f_bsize = 1; //TODO: may be append archive size? buf->f_blocks = buf->f_bavail + 0; buf->f_ffree = 0; buf->f_favail = 0; buf->f_files = get_data()->files.size() - 1; buf->f_namemax = 255; return 0; } int fusezip_open(const char *path, struct fuse_file_info *fi) { if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } if (node->is_dir) { return -EISDIR; } fi->fh = (uint64_t)node; try { return node->open(); } catch (std::bad_alloc) { return -ENOMEM; } catch (std::exception) { return -EIO; } } int fusezip_create(const char *path, mode_t mode, struct fuse_file_info *fi) { (void) mode; if (*path == '\0') { return -EACCES; } FileNode *node = get_file_node(path + 1); if (node != NULL) { return -EEXIST; } node = new FileNode(get_data(), path + 1); if (!node) { return -ENOMEM; } fi->fh = (uint64_t)node; return node->open(); } int fusezip_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { (void) path; return ((FileNode*)fi->fh)->read(buf, size, offset); } int fusezip_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) { (void) path; return ((FileNode*)fi->fh)->write(buf, size, offset); } int fusezip_release (const char *path, struct fuse_file_info *fi) { (void) path; return ((FileNode*)fi->fh)->close(); } int fusezip_ftruncate(const char *path, off_t offset, struct fuse_file_info *fi) { (void) path; return -((FileNode*)fi->fh)->truncate(offset); } int fusezip_truncate(const char *path, off_t offset) { if (*path == '\0') { return -EACCES; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } if (node->is_dir) { return -EISDIR; } int res; if ((res = node->open()) != 0) { return res; } if ((res = node->truncate(offset)) != 0) { node->close(); return -res; } return node->close(); } int fusezip_unlink(const char *path) { if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } if (node->is_dir) { return -EISDIR; } return -get_data()->removeNode(node); } int fusezip_rmdir(const char *path) { if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } if (!node->is_dir) { return -ENOTDIR; } if (!node->childs.empty()) { return -ENOTEMPTY; } return -get_data()->removeNode(node); } int fusezip_mkdir(const char *path, mode_t mode) { (void) mode; if (*path == '\0') { return -ENOENT; } int idx = zip_add_dir(get_zip(), path + 1); if (idx < 0) { return -ENOMEM; } FileNode *node = new FileNode(get_data(), path + 1, idx); if (!node) { return -ENOMEM; } node->is_dir = true; return 0; } int fusezip_rename(const char *path, const char *new_path) { if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } if (*new_path == '\0') { return -EINVAL; } FileNode *new_node = get_file_node(new_path + 1); if (new_node != NULL) { int res = get_data()->removeNode(new_node); if (res !=0) { return -res; } } int len = strlen(new_path); int oldLen = strlen(path + 1) + 1; char *new_name; if (!node->is_dir) { --len; --oldLen; } new_name = (char*)malloc(len + 1); if (new_path == NULL) { return -ENOMEM; } strcpy(new_name, new_path + 1); if (node->is_dir) { new_name[len - 1] = '/'; new_name[len] = '\0'; } try { struct zip *z = get_zip(); // Renaming directory and its content recursively if (node->is_dir) { queue q; q.push(node); while (!q.empty()) { FileNode *n = q.front(); q.pop(); for (nodelist_t::const_iterator i = n->childs.begin(); i != n->childs.end(); ++i) { FileNode *nn = *i; q.push(nn); char *name = (char*)malloc(len + strlen(nn->full_name) - oldLen + (nn->is_dir ? 2 : 1)); if (name == NULL) { //TODO: check that we are have enough memory before entering this loop return -ENOMEM; } strcpy(name, new_name); strcpy(name + len, nn->full_name + oldLen); if (nn->is_dir) { strcat(name, "/"); } zip_rename(z, nn->id, name); nn->rename_wo_reparenting(name); } } } zip_rename(z, node->id, new_name); // Must be called after loop because new_name will be truncated node->rename(new_name); return 0; } catch (...) { return -EIO; } } int fusezip_utimens(const char *path, const struct timespec tv[2]) { if (*path == '\0') { return -ENOENT; } FileNode *node = get_file_node(path + 1); if (node == NULL) { return -ENOENT; } node->stat.mtime = tv[1].tv_sec; return 0; } #if ( __FreeBSD__ >= 10 ) int fusezip_setxattr(const char *, const char *, const char *, size_t, int, uint32_t) { #else int fusezip_setxattr(const char *, const char *, const char *, size_t, int) { #endif return -ENOTSUP; } #if ( __FreeBSD__ >= 10 ) int fusezip_getxattr(const char *, const char *, char *, size_t, uint32_t) { #else int fusezip_getxattr(const char *, const char *, char *, size_t) { #endif return -ENOTSUP; } int fusezip_listxattr(const char *, char *, size_t) { return -ENOTSUP; } int fusezip_removexattr(const char *, const char *) { return -ENOTSUP; } int fusezip_chmod(const char *, mode_t) { return 0; } int fusezip_chown(const char *, uid_t, gid_t) { return 0; } int fusezip_flush(const char *, struct fuse_file_info *) { return 0; } int fusezip_fsync(const char *, int, struct fuse_file_info *) { return 0; } int fusezip_fsyncdir(const char *, int, struct fuse_file_info *) { return 0; } int fusezip_opendir(const char *, struct fuse_file_info *) { return 0; } int fusezip_releasedir(const char *, struct fuse_file_info *) { return 0; } int fusezip_access(const char *, int) { return 0; } fuse-zip-0.2.13/lib/fuse-zip.h0000644000000000000000000001044211477217250015765 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef FUSE_ZIP_H #define FUSE_ZIP_H /** * Main functions of fuse-zip file system (to be called by FUSE library) */ extern "C" { /** * Initialize libzip and fuse-zip structures. * * @param program Program name * @param fileName ZIP file name * @return NULL if an error occured, otherwise pointer to FuseZipData structure. */ class FuseZipData *initFuseZip(const char *program, const char *fileName); /** * Initialize filesystem * * Report current working dir and archive file name to syslog. * * @return filesystem-private data */ void *fusezip_init(struct fuse_conn_info *conn); /** * Destroy filesystem * * Save all modified data back to ZIP archive and report to syslog about completion. * Note that filesystem unmounted before this method finishes * (see http://code.google.com/p/fuse-zip/issues/detail?id=7). */ void fusezip_destroy(void *data); int fusezip_getattr(const char *path, struct stat *stbuf); int fusezip_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi); int fusezip_statfs(const char *path, struct statvfs *buf); int fusezip_open(const char *path, struct fuse_file_info *fi); int fusezip_create(const char *path, mode_t mode, struct fuse_file_info *fi); int fusezip_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi); int fusezip_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi); int fusezip_release (const char *path, struct fuse_file_info *fi); int fusezip_ftruncate(const char *path, off_t offset, struct fuse_file_info *fi); int fusezip_truncate(const char *path, off_t offset); int fusezip_unlink(const char *path); int fusezip_rmdir(const char *path); int fusezip_mkdir(const char *path, mode_t mode); int fusezip_rename(const char *path, const char *new_path); int fusezip_utimens(const char *path, const struct timespec tv[2]); #if ( __FreeBSD__ >= 10 ) int fusezip_setxattr(const char *, const char *, const char *, size_t, int, uint32_t); int fusezip_getxattr(const char *, const char *, char *, size_t, uint32_t); #else int fusezip_setxattr(const char *, const char *, const char *, size_t, int); int fusezip_getxattr(const char *, const char *, char *, size_t); #endif int fusezip_listxattr(const char *, char *, size_t); int fusezip_removexattr(const char *, const char *); int fusezip_chmod(const char *, mode_t); int fusezip_chown(const char *, uid_t, gid_t); int fusezip_flush(const char *, struct fuse_file_info *); int fusezip_fsync(const char *, int, struct fuse_file_info *); int fusezip_fsyncdir(const char *, int, struct fuse_file_info *); int fusezip_opendir(const char *, struct fuse_file_info *); int fusezip_releasedir(const char *, struct fuse_file_info *); int fusezip_access(const char *, int); } #endif fuse-zip-0.2.13/lib/fuseZipData.cpp0000644000000000000000000000577511477217250017012 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #include #include #include #include "fuseZipData.h" FuseZipData::FuseZipData(const char *archiveName, struct zip *z, char *cwd): m_zip(z), m_archiveName(archiveName), m_cwd(cwd) { if (cwd == NULL) { throw std::bad_alloc(); } build_tree(); } FuseZipData::~FuseZipData() { if (chdir(m_cwd) != 0) { syslog(LOG_ERR, "Unable to chdir() to archive directory %s. Trying to save file into /tmp", m_cwd); if (chdir(getenv("TMP")) != 0) { chdir("/tmp"); } } int res = zip_close(m_zip); if (res != 0) { syslog(LOG_ERR, "Error while closing archive: %s", zip_strerror(m_zip)); } for (filemap_t::iterator i = files.begin(); i != files.end(); ++i) { delete i->second; } free(m_cwd); } void FuseZipData::build_tree() { FileNode *root_node = new FileNode(this, "", FileNode::ROOT_NODE_INDEX); root_node->is_dir = true; int n = zip_get_num_files(m_zip); for (int i = 0; i < n; ++i) { const char *name = zip_get_name(m_zip, i, 0); try { FileNode *node = new FileNode(this, name, i); (void) node; } catch (const FileNode::AlreadyExists &e) { // Only need to skip node creation } } } int FuseZipData::removeNode(FileNode *node) const { node->detach(); int id = node->id; delete node; if (id >= 0) { return (zip_delete (m_zip, id) == 0)? 0 : ENOENT; } else { return 0; } } fuse-zip-0.2.13/lib/fuseZipData.h0000644000000000000000000000440511477217250016444 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef FUSEZIP_DATA #define FUSEZIP_DATA #include "types.h" #include "fileNode.h" class FuseZipData { private: void build_tree(); public: filemap_t files; struct zip *m_zip; const char *m_archiveName; char *m_cwd; /** * Keep archiveName and cwd in class fields and build file tree from z. * * 'cwd' and 'z' free()-ed in destructor. * 'archiveName' should be managed externally. */ FuseZipData(const char *archiveName, struct zip *z, char *cwd); ~FuseZipData(); /** * Detach node from tree, and delete associated entry in zip file if * present. * * @param node Node to remove * @return Error code or 0 is successful */ int removeNode(FileNode *node) const; }; #endif fuse-zip-0.2.13/lib/libZipWrapper.cpp0000644000000000000000000000367211477217250017357 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #include #include "libZipWrapper.h" void zip_stat_assign_64_to_default(struct zip_stat_64 *dest, const struct zip_stat *src) { dest->name = src->name; dest->index = src->index; dest->crc = src->crc; dest->mtime = src->mtime; dest->comp_method = src->comp_method; dest->encryption_method = src->encryption_method; dest->size = offset_t(src->size); dest->comp_size = offset_t(src->comp_size); } fuse-zip-0.2.13/lib/libZipWrapper.h0000644000000000000000000000520411477217250017015 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef LIBZIPWRAPPER_H #define LIBZIPWRAPPER_H // // This is compatibility layer to support libzip compiled without large file // support on 32-bit system. // // If on your system libzip compiled with -D_FILE_OFFSET_BITS=64 then you must // add -D_FILE_OFFSET_BITS=64 to CXXFLAGS when running make. // // make CXXFLAGS="$CXXFLAGS -D_FILE_OFFSET_BITS=64" clean all // #include #include #include #include "types.h" struct zip_stat_64 { const char *name; /* name of the file */ int index; /* index within archive */ unsigned int crc; /* crc of file data */ time_t mtime; /* modification time */ //modified type offset_t size; /* size of file (uncompressed) */ //modified type offset_t comp_size; /* size of file (compressed) */ unsigned short comp_method; /* compression method used */ unsigned short encryption_method; /* encryption method used */ }; void zip_stat_assign_64_to_default(struct zip_stat_64 *dest, const struct zip_stat *src); #endif fuse-zip-0.2.13/lib/types.h0000644000000000000000000000376311477217250015377 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #ifndef FUSEZIP_TYPES_H #define FUSEZIP_TYPES_H #include #include #include #include #include #ifdef _LFS_LARGEFILE typedef off64_t offset_t; #else typedef off_t offset_t; #endif class FileNode; class FuseZipData; struct ltstr { bool operator() (const char* s1, const char* s2) const { return strcmp(s1, s2) < 0; } }; typedef std::list nodelist_t; typedef std::map filemap_t; #endif fuse-zip-0.2.13/main.cpp0000644000000000000000000001742211477217250014741 0ustar rootroot00000000000000//////////////////////////////////////////////////////////////////////////// // Copyright (C) 2008-2010 by Alexander Galanin // // al@galanin.nnov.ru // // http://galanin.nnov.ru/~al // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU Lesser General Public License as // // published by the Free Software Foundation; either version 3 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. // // // // You should have received a copy of the GNU Lesser General Public // // License along with this program; if not, write to the // // Free Software Foundation, Inc., // // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // //////////////////////////////////////////////////////////////////////////// #define KEY_HELP (0) #define KEY_VERSION (1) #include "config.h" #include #include #include #include #include #include "fuse-zip.h" #include "fuseZipData.h" /** * Print usage information */ void print_usage() { fprintf(stderr, "usage: %s [options] \n\n", PROGRAM); fprintf(stderr, "general options:\n" " -o opt,[opt...] mount options\n" " -h --help print help\n" " -V --version print version\n" " -r -o ro open archive in read-only mode\n" " -f don't detach from terminal\n" " -d turn on debugging, also implies -f\n" "\n"); } /** * Print version information (fuse-zip and FUSE library) */ void print_version() { fprintf(stderr, "%s version: %s\n", PROGRAM, VERSION); } /** * Parameters for command-line argument processing function */ struct fusezip_param { // help shown bool help; // version information shown bool version; // number of string arguments int strArgCount; // zip file name const char *fileName; }; /** * Function to process arguments (called from fuse_opt_parse). * * @param data Pointer to fusezip_param structure * @param arg is the whole argument or option * @param key determines why the processing function was called * @param outargs the current output argument list * @return -1 on error, 0 if arg is to be discarded, 1 if arg should be kept */ static int process_arg(void *data, const char *arg, int key, struct fuse_args *outargs) { struct fusezip_param *param = (fusezip_param*)data; (void)outargs; // 'magic' fuse_opt_proc return codes const static int KEEP = 1; const static int DISCARD = 0; const static int ERROR = -1; switch (key) { case KEY_HELP: { print_usage(); param->help = true; return DISCARD; } case KEY_VERSION: { print_version(); param->version = true; return KEEP; } case FUSE_OPT_KEY_NONOPT: { ++param->strArgCount; switch (param->strArgCount) { case 1: { // zip file name param->fileName = arg; return DISCARD; } case 2: { // mountpoint // keep it and then pass to FUSE initializer return KEEP; } default: fprintf(stderr, "%s: only two arguments allowed: filename and mountpoint\n", PROGRAM); return ERROR; } } default: { return KEEP; } } } static const struct fuse_opt fusezip_opts[] = { FUSE_OPT_KEY("-h", KEY_HELP), FUSE_OPT_KEY("--help", KEY_HELP), FUSE_OPT_KEY("-V", KEY_VERSION), FUSE_OPT_KEY("--version", KEY_VERSION), {NULL, 0, 0} }; int main(int argc, char *argv[]) { if (sizeof(void*) > sizeof(uint64_t)) { fprintf(stderr,"%s: This program cannot be run on your system because of FUSE design limitation\n", PROGRAM); return EXIT_FAILURE; } struct fuse_args args = FUSE_ARGS_INIT(argc, argv); FuseZipData *data = NULL; struct fusezip_param param; param.help = false; param.version = false; param.strArgCount = 0; param.fileName = NULL; if (fuse_opt_parse(&args, ¶m, fusezip_opts, process_arg)) { fuse_opt_free_args(&args); return EXIT_FAILURE; } // if all work is done inside options parsing... if (param.help) { fuse_opt_free_args(&args); return EXIT_SUCCESS; } // pass version switch to HELP library to see it's version if (!param.version) { // no file name passed if (param.fileName == NULL) { print_usage(); fuse_opt_free_args(&args); return EXIT_FAILURE; } openlog(PROGRAM, LOG_PID, LOG_USER); if ((data = initFuseZip(PROGRAM, param.fileName)) == NULL) { fuse_opt_free_args(&args); return EXIT_FAILURE; } } static struct fuse_operations fusezip_oper; fusezip_oper.init = fusezip_init; fusezip_oper.destroy = fusezip_destroy; fusezip_oper.readdir = fusezip_readdir; fusezip_oper.getattr = fusezip_getattr; fusezip_oper.statfs = fusezip_statfs; fusezip_oper.open = fusezip_open; fusezip_oper.read = fusezip_read; fusezip_oper.write = fusezip_write; fusezip_oper.release = fusezip_release; fusezip_oper.unlink = fusezip_unlink; fusezip_oper.rmdir = fusezip_rmdir; fusezip_oper.mkdir = fusezip_mkdir; fusezip_oper.rename = fusezip_rename; fusezip_oper.create = fusezip_create; fusezip_oper.chmod = fusezip_chmod; fusezip_oper.chown = fusezip_chown; fusezip_oper.flush = fusezip_flush; fusezip_oper.fsync = fusezip_fsync; fusezip_oper.fsyncdir = fusezip_fsyncdir; fusezip_oper.opendir = fusezip_opendir; fusezip_oper.releasedir = fusezip_releasedir; fusezip_oper.access = fusezip_access; fusezip_oper.utimens = fusezip_utimens; fusezip_oper.ftruncate = fusezip_ftruncate; fusezip_oper.truncate = fusezip_truncate; fusezip_oper.setxattr = fusezip_setxattr; fusezip_oper.getxattr = fusezip_getxattr; fusezip_oper.listxattr = fusezip_listxattr; fusezip_oper.removexattr= fusezip_removexattr; #if FUSE_VERSION >= 28 // don't allow NULL path fusezip_oper.flag_nullpath_ok = 0; #endif struct fuse *fuse; char *mountpoint; // this flag ignored because libzip does not supports multithreading int multithreaded; int res; fuse = fuse_setup(args.argc, args.argv, &fusezip_oper, sizeof(fusezip_oper), &mountpoint, &multithreaded, data); fuse_opt_free_args(&args); if (fuse == NULL) { delete data; return EXIT_FAILURE; } res = fuse_loop(fuse); fuse_teardown(fuse, mountpoint); return (res == 0) ? EXIT_SUCCESS : EXIT_FAILURE; } fuse-zip-0.2.13/makeArchives.sh0000755000000000000000000000153011477217250016243 0ustar rootroot00000000000000#!/bin/sh if [ $# != 0 ] then echo "usage: $0" echo " generate archives for product" exit 1 fi version=`grep '#define VERSION' config.h | sed 's/^[^"]*"//;s/".*$//'` if [ "$version" = "" ] then echo "Unable to determine version" exit 1 fi dir=`mktemp -d` pwd=`pwd` # make program tarball id="fuse-zip-$version" tmp="$dir/$id" hg archive -X performance\* -t tgz $id.tar.gz # make tests tarball id="fuse-zip-tests-r`hg log performance_tests/ | head -n 1 | cut -d : -f 2 | sed 's/ //g'`" tmp="$dir/$id" mkdir "$tmp" mkdir "$tmp/kio_copy" cp -t "$tmp" performance_tests/README performance_tests/run-tests.tcl performance_tests/unpackfs.config cp -t "$tmp/kio_copy" performance_tests/kio_copy/kio_copy.pro performance_tests/kio_copy/main.cpp rm -rf "$id.tar.gz" cd "$dir" tar -cvzf "$pwd/$id.tar.gz" "$id" cd "$pwd" rm -rf "$dir" fuse-zip-0.2.13/tests/Makefile0000644000000000000000000000025411477217250016106 0ustar rootroot00000000000000all: make -C blackbox make -C whitebox test valgrind: make -C blackbox valgrind make -C whitebox valgrind clean: make -C whitebox clean .PHONY: all valgrind clean fuse-zip-0.2.13/tests/blackbox/Makefile0000644000000000000000000000011111477217250017663 0ustar rootroot00000000000000all: ./fuse-zip.test valgrind: ./fuse-zip.test -valgrind .PHONY: all fuse-zip-0.2.13/tests/blackbox/README0000644000000000000000000000137011477217250017113 0ustar rootroot00000000000000Test suite for black box testing of fuse-zip. You need the following programs and libraries: Tcl >= 8.5 tcllib >= 1.10 BLT Tclx zip diff valgrind To run testsuite invoke command $ ./fuse-zip.test or $ ./fuse-zip.test -valgrind to check memory errors with valgrind. Additional options can be passed to valgrind using VALGRIND_ARGS environment variable. For example, you can ask valgrind to generate suppressions (that can be added to file valgrind.supp) by specifying the following variable value: VALGRIND_ARGS="--gen-suppressions=all" To get list of all tests use -list switch. To run only interected tests instead of full test suite, pass test identifier patterns (glob-like) to testsuite script. For example: $ ./fuse-zip.test 'truncate-*' 'rename-*' fuse-zip-0.2.13/tests/blackbox/data/bad-archive.zip0000644000000000000000000022457311477217250022051 0ustar rootroot00000000000000PK:D<9$(Nbah.txtUT KjKKjKux z׵')B4%R&Hg"-J8$xP@f3$O_M}}?J=I5=$I9̈מ^o}}ϋpVo^WomW~f}t/_ewÛI;k|?}<5<N?\gjcgz/YS=]4Mu؟,/E_uU{d룏< MWuWUxҟKdz ҅OXK{]^CCot`SìR wc O6mIl:Y5y~ P~.ϪtL%cj׌׃7&Qvf]Cs TW LY&YZ#{6ltXMaR&,Br;8=<:9}8gYG눦>⾐ho9u[Es̓V_/I3 kG~V9:k|1iA[f6Bu))x"lWvX =C VjA u7̈}࠱jv֤A뗁熕B&#1,EH.NoYxGzMNFTɤ/i1ܳQ~hE'sqhF02 WiaRpJm׏ipbۙ/0}O0՗tw.% 5^4FDNONL8\ х?C_u8$_-< ]~OE` K"Zx!lX0ϫ6 |x,XW?޺;GX~ Lϛz!kXq)c=灩]8wxuz,^^p ez ff٠ܟxA⸂ g5<<6tiOx_v_c LԜ =zqOχG&m|>cf ͻy29V,p,a6@#P7qKY0? UWx0U6ZV8A8]ڮ]|1>h=W7>v.sa}pLp9׳/ĭrrD-bcaTp㥞6QsXh[+iQ.zQfo@NwxSު; xQ/.1<B/k>y75޴ȋmUƸ*xWs0#E(, #;pAJ̗}RU%U`;ȵ~¾IA''ߓLp~YLS* [% R񇤟3,~x_VFh8djYvr`qXf@Xq?d+:}rx_TUfb㸸xX'aYj[NJ?>Gf 8>vxc)`, >c]"SKвÿŬ!rmFf ,Yp7Rm@ph[M> mg& ^b"p|a6ba\cD%*F{/߲^daV]ME/HJKe>9y=E1/Ngx ϋ(r/.G`&"=&!ykhGߪɣ#cvĵY/8َN2sPYA1-”Qjv킲/]FA ЍŸ`JgĖ/<[, y`ᛅ[Ԩ64rכW YUa[<|( (; l5c7(tdvh!QŰ %G颞 ,<cY3'(NðDY|RwpIeahmgSOרڴ|EiNF![roxo$h/_14x}&eZIF6`[Etnm$% Z68ѨRj xꯓ.F4`rY&ς12l Ek2|L^ m\=5X6Dr:g#$ekϗ$C 䬐qѢ?NJӽo}wKTѬ7Yr [Zfrd⬇H̪(>Y0.E㤕G6sR2;՛$RlOZԹ6$ZƳ'DUcAUn1-Q%=o'"դAy,Kg[pɎaJjy/{O=Xmל\p%{bJ&\جY>ƌiJ`Nl0_bkNa؇ , HeW1nq[PuDZ|5,_P\BUPv.qo_rD&(`k6Ѻs)9"}M < `p'Otxw6  pbϛ#m#oA,Vq!=Zbb4l|ܥ(R`bޟ;Er lff'|M& haUK\vƚ_W6ׯ7o>(zGN2䑷ȋǵT?%4* U_ۑW>QNPSAjKL]4=ΦvZ3"D܂tNֽgrx`IΜHtX틑D: ŞȦl`^y?,7=#BI0ASQ'_ I="vWRW"Ӗݮ05Q"7*{qKD~]Y*ۄ_?Fs1ͬjlٰgfS $cp`s b)A^ӆpsEC&@QPL0@昹Ĭ9:{t]ٲF`}0`x՚ EG)O;߈٪AXl^TV5׆1n2y$Kĕt4x@õ~_ ^B7a%_٬!c>I&}x(]>t ;vzXX'sÙXHv(2u&U>g= m0@M:_SjQ݌4+gR#m*rڲ/hf"9\W 0l@4t́$ks6MݝW(qn`$]IyE[Rp(yLhKS69:|>2 0-ro+{B\m^f&s$n1ƱE{ T}'ȓYUh.2qgQ2Ao.Ԁ+9 ; q)|Lh'aj8lNLx]ԐR"O4]+_Nj~)2TъS+BoőA FYK{Z}*]#jB)7h=80P%2- 3>cVz󀋾~=+eP46*FB>!fqFl#/_~ɒ_}$?(Cl{Rl~~ kڍKn94\ӰDN F?*͋McKv0r04e&!<_~;h}h HX洹EMHvR5a'HOu}q=u;)Ktj%v/5 f5qlh _K_us)aMWzyiq/s)F"Ϸ V˹~J Ȼ@abe&RH+ J[ cJYL+ukCn_$`c-p.ooܥ `ᶴ?0~egiBIXe7E>>V@C8-{- X[NP"뼻Ea.ӌoxP1.(ƢlI6 M GF~SXd,t["b8` 3t2[I$-(2f'lh4Y؟_\4I9׷z`ue;d'_D=[rȬECEXK#ndzRV䤝,[(S ]eh^a&e]a%=툴xql㈣H*W @FM?`[nь8}#F`+ľ9K)I`Ih{}݃ʬ6k7Fxa6Bx%ha5[dr|yC]xe9k߿67q]՞BTNDy-5X~XkNe5L?@ָ]pƮSn+zC}4'ѤY._|@T\o94a $M6 }C3p I=ksދ/ۧoj]GQ #d:z>Q[(@{Sʦ'5>jnc(# `kĘn|'1ü\;Ka'ᄺ(K= ^M>8r"+Y5RRO%q x8h)m(%sFցL +-[g:Xr=}xrI/7:,H̫&yCtٻsOx@L.(={t@yF5oPa7D)fljr)fO'5O4%mxX4 ]\5yXcsf,ίK+{MJ\`shs u|C' bnhM-xc6b)T{Y+-:ømUW&mca^_x̘[NJĤ/1I:t"ydvLtE6T2XB ъ[<΄E_s"_@}ZjfdsV-!X QjEf4 cmonl*%wOc0D C#h<(>m/W \h0I|"񮽪6.j?7Bz?>% [Yo4K }T8PK@hX.HnI/$; EB)˞eUI@%aW2wuH#rȻ.ECL,6$U^C s~RO\vhT 47gNJtO}B&ӛI^ l;企v8s&u1|F( چ@;m3i0:漩Œ*Xc4\T'b8HE{]`7Ψq`FJX\aCbE2+,|*%k6Ic#t"Gq5]7ϔ̧syQv-C&"(#6 C-JE֙. 'M khmaDHY#NK/ ]\D@o)bŒDj!6fX[1ÝWDmV\aT&_p<:w9/BW&uTHZɈi@ʼU)6a>5YYT.(w9[dVLH]{aW޳|=d0AUݽe4Cp}5ZN*ń5bl,lT򚊋/Ŋo?MX"_,H=1d˧O/ d_gR]v /& _{NŢ?dHZ⯳~ 6<<M?\1L ;}1kE& c@0tWBp, =c1mOV÷ޮCP{}6`y:|_ 46?c@A5D0iQ$#? Ibɝ۸{I R1Q՜'o媃P$JM9HZ^U1 u JP 22IῤI4a l@Vޣ1MPb^tIPf= C:!8>RuJ?_fX[ä {އ$8rT#*ݤժ'3M(6JfbyCU>! w~v;B{s F~bȖ4 [Q-=i*vbISQdg˂t1ԋS]/{៸3v P{ocJwa"D&ut֜k‘T1Ҵ`\u'/_3R[s^+ۗ'4wV=U^^f]wWI\bE#[1"+R P!@tAv0h5F~xB+^l[$Dv@!Ӯ}z;"w,ߵA(5.mj'QG6llܘ1ߎ@Fx`)||Cj,ě=Xæ$XgZiV>R7|8jre|^Tڰ NPbrbiTg;4ʆ`oߟzȥm`hE r"x|G2C?Os.*GPϋ "2X&Wd:PatO '\YEو5:%]1,-|>sa 0d2EjdyrLΚ;Byig`gEsϦQ[AP"6 Nn`RE|S}X9lMmpBnb-"Js)$\B| Kr&  a wHC?%v)fXJNE~\2Ǫݤ嵄[!)">CdYuۭ Rtx;g/J%hbEyV;JiǠ(E+rIB 19)k;'' |.%5z]LV w=5Xg+BhVU^WNCdg{ۇڭwހD6:@>VOʚhe-$wwƆUec %Dd#2P*#XKd|Tqn-^#I!ֱZ H? -֌j6d8FCDI-{`(CԊ"J=z,qcnS s/z:B&4M6Y؟Ky:Hϟ5ZGo l Zmk[3. \QrX5׆|3npӌX.ؕ3 W5ޒ&.X$l;wR]~\8 om ̪65La?ƭݚkZ"jm0B}3_GtS#sFu5L5tkԎ:o|{􇗻[;kz=\0PR؁L  (9_Q,0w='@ۢp,tY9l6*'1{/?pnR-&I~8v܎=:q,+ %o :: weI8k~TƲHV{kO; ?g_8*I@&z]ݢirU&mJ@z"w+]߿"ʼvx?pJ?C}US!8^IKzUG_CGoGW  U}< ѿQpa<-~ɽKtقS~BdZ'.v3)* W+Pe 9*Pip>Jx;Hg 5qaMq[9$h) MIOE֑ix#f0@Y 5˝eI|kKK MqM۟zWTHR`m,NQnQ|9b|Ÿ`vd{'L /)k[ß $,RF*)#:Sc1-\kVI©8]uQ +a-hV8, 6kؑ}ʣq$s%A$r, `: x^4ۘʼn=Hrz8)~&uƔrpZZc Bw*|'%BYqaX)7W2,)0%nbi1JB!--WۄzyXd‹+K$9OOd+iIh'Jy^K·Kѣ]+!P8[ELn@Gs GXh+ ~ECp6P9TI`p֒/;u_3p11URWl%+b (d˦(_Ios> ABM eo3<ʏxeVtFF&rn 1 g)取زno ()4wn|`fs;d]-h?{'dsH ;cTu& yy}MlT5_%*ڝ6M{/}؛D!llxC']Q yr7HGJXRZr+>m 9\<`;V N9<ڠI!GSvx|r)mb ]J74`T[ߘU`}0bz(mw :uj #ꏓsH1H˟O^| Y$ֶ%Bh+<^}0Q](r\44pEjZm. LU1GPS%*F#iug+V+DLT$#2]} ǡ^|G.BCom}:Gv9\EևO6:ZT_99 9U GCNjDs]־4z( qHed")N1£L*5 >@̦ym@[@bU%c?:0渖W *AoNᅥfa,r™_f%pVwKɟ`tQ[aSiKRye@K9wT*HKG_k힤mcwհǎ`{gij;QQS& N!sH Wi:23PS`6Hp3|ƣqqף 9AePP.P E)/z2H }Aa+Wۦdv.DpJ%YgW_+g]zO/ʾ M; Ǎq1[h1>9Ɠ/6ͳ.$QÔ|jl1ըŊ#\$z2 l Kt y ݢPIgd(ṚO&v͋@Te \98@+9yu Oe=LxԼI|K@d8o!Z㚻;Նcõ 'RU3sdśHzXk}ͳo ;wF舺L~ ӄGTO9$RHܒY6 VɆ*YL~VGiz=<8I:cZJVSE>w&XR\Ea ӊH#gG3B@$b|4kBȶ ÏpaҊR7ԘޝB,%6S6"Jd"u)nD߳`c.p뜰Eˎ+73)*exWh#2[*u B;DptlW|@50KȄRV" c&eюrRCE&ɄZ8I.E njtY)һPDo_;ՏL^`EGnC4zDzKWDl{4f13[-"9`FGI{ Q*ނ`2I&M~P/..0\p'wUx41[}@6I @/aȴxBPD"Z6N j4v$[V3U: jv3I̬I-m-nA%~QE檢FX'( NbfիPMx6U|}"I ڽ e*EQA]~T#KTRRr1Iq9|AR/̝c~̗kX^o=(~ c| q|4D}YtMhcY"&p%f)TRH]0J\^& ^7n,CR;Z&ZP2 jU/̓Vt;E}Utf m0 $Kiۄ u(Tpµu$Ź(Μ)NZȀ QoP7+-,s T{/Dr]l N++mE )z? 0q\qY`v9u"7|:1E;qg">$2=|@KS{b'-_cP嫣F<3Jy@fh Z⛊+]Fqw,VQP#QVl3rG& GhY5I5^@]u raO1Ýg϶r* DT ͸J>gd`m(17ru8ke&Ӷ_piȉ!``GRdc9U$3"BdL [ rGZkƹaܐZ"|p V1U  IE|&z[1 {?˿VĻ|{YuEò{8z0C`T@z;G BODηY!lo%+^fo!UqЀfH\T"@VxGJt?WD,Iվ$rq0e:jI,qij5%N"g%MAm &N'b)-o?Ȧ$+uR`ll#mC{~/?>?QٝGK8~{'sFWKPņCй ]LB2mxUvIUWV͈ m6ʻwL(y{p^LyȜq8^FI@CQ#Uאi`Fs:"iݛ BANjE4 qeM`|smkәR G-ܰCdk7i54vnHl}2zz18)/䍮‰/¶ /$F4\aI8) s6K7 lt=! ivvs59~d8=Cvw k_~xS$uyfj Sv1zobi*!1s4sY9Ey} D w`~(+σ/֠Uum{#vVއX8H:}wpt|}\`Q  "I бA:4wɅq'kO ~8o/CqL{ATv++"HC߳Y;Z<|rk$jͣb8?1ZXջ5֣[symȪٔI!:rb~}acb`-pe~s/lEz.n6hpJ350]-(ޥcgW(?TT%. . C#Mgצ{̞DNtre/B a+)V޻f΁#k;..bVLBQ]hE"ң,$$ K bjӬ9;2u9{@%O>LhkRdVx ?q{tլId6G]Q\̑j-ԢǤ]H^] ]r. { l]/eζjvHmSA| *xwdQ *cDj,p:]=xI84酔_ (NLܝnpv+%\P0íO%GvMwE81|4B͈.,49Ջ-x]O~=;~Cvy=*+*9p5@m[{!2ƛ,#-b*ȩ85\v7dkq! G l J0đDf'>DQGRjv[),T%w&S8o5h R$I?n8P2]:AePQT ~W* I\j&d3ʥ7ղ?YxlVdI=eLRvџcyHN'!Ř~3H)ET3ujƎ; ~@)>?nOW,*LOXddö`>b2mx0.NVO |[q4m\G^LI A m* a]Gv1.E%N7ْ 4:I'5Cs]E=Tpu yW0wB ו?o(^ס 8,ogc }Bjf@, {|6 詊 H;YLmKVHO5W K+C}&!JA_9ڄv=tFTQ|83#ϫvn {´(jqvƢKQ72(바c8 ]C! K@nG5@YŰ 4 qSWMF" L![ukhѨnӯa>kۥkk'eP4"AB$\tX<44GI\ hj7MtT (G("4ipZ%uӹ$j\P+D3W];'3u=)B[8MTԮO.YIa{)ӉtO;O8!*6K 9FÑ2{Qay6j} |ܲYi7w!q^ܑbviN%jx'F}Yˠ 5R%nŮ!#[͉KjY{'< $k6S4k;r\W>F]7x >ܑ~fkU^uY.Zs!eXcK \n[RUXINؠ 398[=F $NP;Jo8!~[!v\LԪ7T>^ۀ;R3;2F%|FK["'uړI/C(Gqv_IO\ fKT!犔Ly/Q\+Ppldo./ R` !J;) NJ L{{mvU_M;J 9|߷X$1Q&O$O$g4*崂#QBvն(>@)f˜AB[Y8#"^a5FJT 4/` RS;&$qMc3룚@#߁2Wy8Ӕ~u4 ۔$\d̉Juj0gEyע=%Hl58_ ͕W U~M v+io3бlp2֝lT`,!B*jo¬Gwt]n䝍$!=.f? H_W$ij1;9YU:[V'pn?}kRS^.2-'y#fNF\U\-$X; Fi,DƟHn>&AZw;p @Bhwvk=j*'.΁ٓ 33 CG_F(2ϙxp)4Fg%QOrˎq15C U )1@TFOsl64H;HD[ȼ4R-0¢3Jfp) 6Km "2-!ءۧ!|]kţQ(D}&bBO&FIa&~iok0`g[[nxwm>RU$;j$DwEK _`rk8$GfaBqɎd9Ѕ]ɎF0/ n(`" &xȎB:OKR-^@{ n8< Τ)1YbFds M%R9aSg7Mq~ƶfvw'o؟J&JsQP ;,ƶ2Z%DxtAns!In "N[~6~ nR%7U@]HoZXG!GasVO! vI`Ĕ$f!(Pq~)+(Z$ UǦ>0jR `, ^tIY`u[_bHX! "g^u]~F7>'3 /1|QfM՞{uU_k2,P،9a3 ^6ibՄ9WCȱFck0jΞǯOn 3,&!_t5#@NВ29/jчka@Z[lVvt0Û6y0p s(5Q0\&:,ʢ/\lU9]( |BX5#X`bYJUHP*md4aupL(L;ҫ 247E{OSr4 -3xZ fS_ #) 8c]YB[jřf'+voǽMҬ-|X\T&9@1Dژ-VgYSpI+q;@IڨECOFjv91"GcE.[Bb!0*-X*Lt}qBysГ-HOԠ~k7[갮{YYpL7d m]xmf(^PiHqimhe;~(lMDLRK/F=.UIQ;~~$2u g= [z#E]V\U^3(Wҷ]?l0'L_|̕20Csj`5q$W  o+'ݢp EYUE/.u~XUI=P6q07͈ e!!d쉷I X=6]/ l 4"R=!BP75iLDH@҇ 4,pa;8U{@"F.5A WC C< (" 2ef#k(?h[ 5'R kRz/kyDL+ZwFy %{Ғ)%{ː]d'ۆ]~D]HˡPwh1Tc;>{A"eAڗ&n*Nr;ec/lm{t"c1$bw@0zaqP,u`g5'+.GS:b#TQ;Nﶟp/1}rB(E589- n]s"uħ]r!"51DQHRJ+_E'C4 v9{?f5 x{S17CRhHm$Ʌjbw_u]u)rCOhf877J(q:.ع&_TZRW5L! \pըѭEdJhs@N(X!OW]צ:$uB d(Eࣂ>~ٛt&I)@[z\ܚ;d4] 鷀Mm!01*@_YP1@Vas]_5ܘݐ9jD%bdZI}}-C[e$wM(p}&9 ?** \R!x[AҠ:l' &&E W}Dڝijd ~ ~#_ou^f%} MH X99+̔N} 9DkH)@͒vjэ lʊ[.nt`{8tL>YwL>FDŽ/<&BWJw/_MU̺bDw '%^x^,n>qg>ETv\g~^7n4D zeu/޲5st$kFSZ\]+yWBu #V*7`^ [ Ոv9=EX'x)kkݥoW XgF#yq1{QD# gG:AX27يeK`~+~&zYY?xWw\S^7`(78 m Cb>"6= ?찋q8-Z!cN('c^@Cq~w_\xkFjA[Vq U|tYY~9{i$Ǘw9zǾ8~F% U4`8ӕ9Iqa> j'0z}w7~ MJ! Y\U W gPr!swT8^F^*xu\!ڮ[-$[6'pDkJ{xjCU!Ycך`^:G=f2 6Ov ?u|R+1?Ҧf-c`9¨b)ƪؙ*mΜ푼whF'kb%!?kf 0Jw_S6m`S@>a9~csƓ5u.i \^c B<⬻\}h6#ԅ`D=tgv.,Q;}Dj:hPs_(/@)ɕdEWW1mw,sxץ_!â(mZq]wAtJPm3 tUS2Xg攸) /geT_'^n;w`;ݻɶn ((Qxu],GX0y2*AG.T=^rDe)3YSELΌDAEskIbU# .rLIZ62-Cw2_lCʊ,_0)C kP%Ñ̜֋&m 64s+&P_Y%v%e໰->^ u>A:TOsgf|enLMYU/M۴([mL)J E@I[ee?49&2S,W&RUv(05u=˧ޑ#CIWlz 5f|'ճw=>JȌybXtΪo :o7WBץH+(z m~)KGSq>7>U±zvHE(o!t8q65&[Oς{ :b-%:i ϗ&lV yǢ̛!qy7/vTw_m km gѵ/,-xmjw<;z<`nTh B]NbnE JQ?ߠVqOs:4i/2 M~r^mM_Nz-8%dIl.[#SF5<Ê5*HFU~v@Df7iMz@8U@C|uʑdIkS6kG)"X7Q`O.pu4 B`K#_ 5 % .O/`'{=32Qi,(dzj7D:$s Vi􈪯`um#K,Ag?v4ܨoS4w~|F zY _> wld{L^$@ϐ2?{~-vsh=Cn KG>;9rE%0*6}m䜾%(5dJ=Hjbv;pxKev j]Vy L<ºEb8. xK';xK+ ށ-Fzkpc-Z9W_}:&cQ DAN(Kđ,BVh|dQ@x|sh#hB7[>WD5i f~m rQlen6Rlh#ᫌ7w ;%fQp30ȫPe*DW͆?NoPDFE[P(R+ KռBK4bvT.}۾Ӈ>L(nYsMЫo)P2!qՕc.,?]_!HdW>kW!qJPne1 ZbtV9+Y \1V>eKa9iʿn\fMpA{Y?,É];b[r{ ru࠘NX$aep=#I_`PM;i _@h%fv$/k8J(0 GB:XBTbnBpo01}~J S'c⚉s,!e0gqUGw:#A/ͭ.W&ޮE?#>Ө(7>00UQm{YfRSaPNPIY 4+(ZMo]?ȿXLo0nJS# X?Ċkoe*6 gs`FLPsU_s-6=\gkO#ʳUMԺ"|&RJn:bS z)_fNhsLZGd} ZV6˟~s삯>>SɇX6G񕺇&J P )DyzrW,BD~΂ 4a η!0X8( kJ>Vep"+(h 55R썸DBH0y!Ԓ \jZ\ZruZ7IΊ)sCpur6d+E_'r#F_FT ҊկPOéθ҆hd6(~:;wpH-c`p3a+Dj/FL{ERU23= k Q䳙\2jh~|`Ձ.3֊'Tˆ[Lk0Fi׸ N=aejˠ2: d L[ =uȑv43آ 84śU=8IU0OfѶ;]pq֨yhD'g$C`@`UfA9Gۤ]LVdorW--iBn2NIasFPSP[Yhv0,|P=)('`gմ} џ ԜS}T@EC L.%N#V6m#ެzgFPttUkMTo%, EF%E eYPX a6l?Jas.m!icGخqvZmc--!HWïU.cE{k>[k֟LFvIydXK }fYݯ>pq*_b;>UB3mjuD˛T_}UQ(I gEa/B2ipy/>wC؇$u<)΂@-1&R#/>fe][GltT8ϾY}e8LQOe/(o !ՄY|ym/e`1 I q)(rd1޹-EfόW;Გ,lބKn !JD@ŖW4*`GS< u.D\zo):M]X vUr!V m.yHUr%tDrRU@XI+Lz^P<ؘUX]_)}Ck.~@=Y M t|4XR`ɉ#yl'JX;J)99qDyٿ 8XK v1m6S=*-AA#x$fq=,Y1-ҷ~;Ǒײ d^E b~o#!J~"H\p2[ǮA. WD@Sybw#c΂ُ+7{@,x䓀G\ܲBm߲ARh0oo~v4E ?N"/naQ⪹^X+)4w$qZaA47zeQQpo!QԤ7MLf}vpmw 3 aJlEQO.><#xoh26c$AIGH Kk($twrVE]ےAHJoiJC`h+Ԛխ' 2j%N[vPϣI(I5d)(7[>Au^2;VG6~G"A[` rì Z!ߣSDý/CbtEY/SMzpnі9kkbj+t]V^n(.F͌(bsD9q VU|QXb@)`b>y2$e\|;s?' gJ (G=a_6_hsܥG+0*Υ*ڇRoL\O|&V/;|őhŤ",_#`[Ǻ*$ 'J0B#XJ!oelXJDg-_U>ۓ(&ضtqRwNh(rE ySB%r&gQ8\ٟ$hCD&wF,dzRx@dn 1_4"uʹY\O2[Jguf0$ $F P3 IєF]Ǎx tnL2p}'DSMfPo`T űaۡ {5\3-z 4Z-mKeO.ne?vqrf'g3ÌB P~tH0yqAng+?˪!alM(SUt$Y PrCRoI>CԑW_@t'M%kW#4@*@˙jL7@縫=2Bd.޷5y/N7DCaldrnsF@&Ŏr7]y/ZH``l"/Gtud(g+/M- 3.AXdU%H?wbuxqbM6 G?]6NP#녎 ?&6UhRO!Lt~:<>zuo60K]9A9褓ME.}-?8GPe"{T+8xYyQiܖ(0pNl^""Rm~V.b$C6ƾ)3d70y!>y^LϪ$p HGSYEh/X\8jaC:DWEWD lg |E(Se^8|1FPaԯĆ_/OqzU=}BXe62Z(ҔqlwL@u~(v\ACS [Cr3%5>)oUGC)^SG}Pm@x B{2?DLb(RzoLxri;,nK\2,_ja $98 ȉj)"Go!両7b7.~batm2|ǃTQ5ciYaB~.R+z`Au- {qIkp{_-91ָ qTAhx z8X,. zwD-ňDAP 8:ݽ0?lV> L@ttRPY` j9 "*:fL |CE9>eUR,.IΕX7#Zr,?L4DͣT6Uy)j_R;7Yc14AǘkT܇VNwl%O:[D#rH3W q.#S>>(R9Qp {gE[ODY)?FS*=JXeX{j8E͇Qɤ44LD@c$I#1#@ګi Z GFܥaU?/adᦱN5#y@1ˈ=1T|ՃD/O`'Ĭe#n,e9uNP(@&z (1gBjbE1t:t̩49 z %TU{u3P/etrz` 81BFuŒ!=啄Q:Tsqs B՘ i\Xf@8=@þI4V0UQ97659c.@q2?>y(']]R%o {#z5E&f3B|6{ O.Vwx(*1&.URc{pUA#N8NBpmxǹtrpTƭi8 K-ДqH#gFhHe{|I&5~*cfKf?N#y=|/9 v'k_'_pfa 3y7~rsٸ#Z(Qt#eqDX.5V"hCmsM$gw=-m4u6Yg:'gA'zq4v^ַ['lQhiKi3yD4 a`W\~_AUZ{0Ȼ pBPoTo㗭RQ;숰q`o=X~q~wmoFW6Qo׷$u3blӷ ԕl䭑69a.ƶ.gi(zA\;),5lQ;]f?>;8:{o+yyt|1GL⻓~e$7z.:, vW#'TWw3&uҵD}'`p9*6Mx0#a}7Fن%qi-u4N@M7 l7"$U 1YpV dUњaW2;HAhj(5>PKV'`(;^lx]R(d$>TVtZ_6`dқp4qw|cJ_Y?M0t{USFMt E`_. q'QSVx>̳A&qu܂h'wZKTQG*6}X B=} 39b79y`t('mHrkopcJ sȳrI{e H8.lAR2 DA9= ZeBj@bχ5ǯԛA9XmHEaS6c𥬺@510:ڊZx1OЧ]H*-n.xGB!Hu >Nk#KR];B [/ mH;(ᐪ={/^rvࣃ}$s})L23Sçme)4Ǫx-dDห՜Zm3'áB",S:nQH_20ǖw˲餹У -+82‡[, s|)O5`ausw-y3%{'!s{- MuNkso O fnGGt<;9FE/8H]ÜF= mN?X<ݧf1^WG~Z]iZNaL.OVZ\4gB#sU`W_:UOv6UϞ>U/v~ՏG\yT܁ly9i|+0<8Dl]|7_^codo&?c_G< =1ς`$ߧ]A cNg-FCAa H1JZQu+DT:M78N$2DWr"xz A9.hIc*zv ^=38}y߾q߻g ҭt"`T݁U |!(Yސl:s9ûmPpgȟD#\Pi .7/^U. f#Wk#IZ&$850Jws~[qR̝#D.-íߜ21bOV Z'K_dMV$fY\%xLz=C.F@oEcS:U0VcCb"?9tN!@ +yhy9h6)pJB 8121cd"tTx_Y5+ҷ#5hdv+c8[+~̷TaGR[ Té^ӑBM\=SFLh0 @qȸK)k8qqPnBd `Vtn,'z.)-  8&% D6*O _͒չ] b / Å+-=HFii.Zt֞j.TYxPop)+UwűQE%H!y{dN &0]͋vV82-p!&$RnGfvipB\{BYW씤pBKasĦ*qZu6۴dacPAlJ k63T`l,f74\>ý)k0Yz}Qa#ILhS[ X|+{%v8G5|\M[AY#("[]NI J{dq#-*ͬ^??&ه$~YgY?/%h.M8"VBE_σb1dHϐjmJ[[*AVH|Bxrll,q_c@:m)5㧪vaS,,j ^@RŤ53K`T#xZl"<Lu`uӟƧ̧h!4͠ !# mri wp`<X`U_xU71vCsCw҈b!NO:?nTR4UJ LXR:n3 w%6YbS#u w(r06*TC5ߗtzKƺ4ķ(E-E{B%!Lf9F81|.S)] @ it]@Iv觵cb+:/J:q9Ww}2 ><%8T8MLaqO=N;A ȏ,4˳Mksٺg* ԒarMdT{gC)p ͅ}lGu)R *C) ~qp.,XSZ틘;ލWn~BnG_`X9 ]Dhë"vDP"rC/1[_zy&N8Xo1qE"rFR3^c|%0ih֎ӱ-I/:l8/R!qDSY8 R5|3 ^M4&QONσds <;:5="ɑYm~:Ӿ,R+a8HfF;%H 4q\l+/F &PBYX8gU+T,5SKL޴"tXyu\qYօ<"%_~~ejs@UؠYB<<~br \zi"9rYފpT$LE9:]c~o\G=X|IXЍ$B.M'k;Jf6`QLOa0olh-fyYxTOW1^<О=8Օ/z%(_p}tNOB063Yr?Wn@)nC<3, L-Jt-u'f%wctiǿ!&gE~ӫ5 o3Nې[i!C3f=M"t"eO`.^dN@1muTL(W .h{;z!\8MS^aIѠ/+P)i -&P/FsY :#t[@5ċkT i,( AgJFb9:|Xny'yvƗߝ A4Ž,^'Lqx@PJ1U7r) mPځ >)YmV]C#el.殈GRHf"|f5XW\,qwloe`T(Y,zE| ~r>cX+l|;~d&,rxU4 m~Bjţ<7p"I. '1pk @zs2f(Z޻\^R8Ii8әbO3!Hfa׳&=0'+|qA%1Wݠ"aR~r rgg:?STH$קd\BQ[UO~a:qDvM&p!cIh)a*6)DX,ԟX96Hi3PnƪdVTXbUxFWVb&*gp|>K`ՙ«8"vb(p}!0*ƋKKıG1beֲV.~wj٨d/')t]#`=c8rdfi:'=ҭL;ao;2avn:t[5HZ&VwinN7}5oTC}tv*PjV7 OQ>d+7n$Mkc({Y촪*~eYT{m2}FZ:zj)C\ԡpF4xV5G!W0^ uM MZ9A8w5G4 ҡ;N%_Ag)-3Z#DVe}TBh/-HE܊I=ohDN2߅>󳨜܊Lr6pBQ pDKdY;i}ߔ.BD0VY&nXMNŀ) ~,! bi)-,|1#ML^3%B >ؒ31Fyՠq3R>hK>siEY t Oq- HO.#jh/ 87Epтl;R'kLәGFԄ9I4A~?t7CWcqbz0+6nN%2/:?;ቬ5zC+↯ vsЬKb4rEQLLb }p24v K& hհjd鹔^!5>&(/ n*9SnHyB(!+2>#CT_%NO-ƲOfT^+ =J1DB- \]ob(F9\z' r ]ƛz 18rSDf-j@ 5rAZtR*hu6]eN#K0ԃLJ+mfiVO֬TJzlUv6Qb̃>֊K4wgӭ#1y+NKpToH { "Wn.tYnl 1܅\Չ) ݶ]RsQ+Qg.>̀8hM5xT^yNIz>CC[`͚r,a<,&,f;Sz_/0>,#iɴ3)LܛBτ𹣇/o1 PLw_a /_Ƣ" kH[uUHmX\$جPP怖՘V:]ye__w3F xY?ٵ,}+7 |q:)o)HŦ F㻿PB%x&b M:&[=i:&_l"2jt Tj|>Kht1B0"x|*PZjr.x5>qҒJ"aC'_l~eP3N wWhB7ZD3KBZD1%.:bH{c[C(GP e0]Wa敥rav"An^UTH m_J!U 2T"S`z;Tnt_cr\) QXo60,ȜYfA -YLA31zFV-pF»Q1Ba)jfaֹ~˕e% T$7ޗq_E,/=(p8Z<-qN3;uH4 @x2ְ1 {1C9f\1{єp5did;_TL7b4y"Ý,3(t$:BYm8##9=`<&]r:oXbҳ=*F8D.]uQ{Q8Hyӑ*ڞ7ᛪq>)vpu)1M$ # 嚰8+3`Nv=yC&8)g/0kS<;#,QBq4DOS\JByDh3E ǯ"\KSB XZd&@!&(WYE)ހ,U9×\'$xжC:D%l*IQhmWd,h*:hb6HzS\ea:0v4(Nϑ$#ȲaB1 RK͸"|ZqKhɖꜥǭlZNtgyI A~-R0<?6%\29ׂ-ZeR/PTcJj kC-JUI]xT]TqP 5zA|o hj;.@x"]h&5wly4G]Υ-' r]!X&S1]jKb~Y[i]`vEf~ ,HS8PDT괲=*8DCbš tII]:WXE'"wRpY֭/h%=F\j9Kʭl.Mj +n{…乶`#*%?E/ ӎrp ڛQ4+;'9A _ (7A K|)O%pOŇ>{V}jދjgOxj&#$Yk™FY\%zE,"M,%&c 3&⁌Ɔ-Vl=;DeY1J߼c$2_K8o)ZMr_¬gdĵZaйmADG8@,,,/\<5 T¹R'# ,;b[_ RD *' ɔd7PHAi9Oy(4\uPI]\8. rkR%X\ x&!*MSsꚉfy56]OaHL7A-*X^:Gu*s[S]{`t<2†K]{,s==R=zPt@6]OZւIq|K푽/c B?_8h?*BTz ,PwVйslC9%)PځSpЯ9 :i. :( <}[={ry.w1Vw w ,uCsaK8YT9Y?v (އ^rV9eUSaXjx,/S"FE׋RusROUꊨ!L`k|`ضmb-q X!g}ѩ=w%*4$;59a=Qa*%8&\9X Иc0 ur_/ f4qwW3.sdSQR*.bʧh\pڳATO e{M?Ȗ>VAwV*VVv 21KQidϒWFP P8Z=d0X2ê/˱laQm fCovܹ@R\^Mk*>ARf[*%Fn,nf>[ [P\ԃk݌g.*R&ϴ oJE8MJ껬͐ltX!DNDp6?hE[ Bgk2Ʀe}H43}uB&kJ, #/w+ڞLwZXEW6OfP*,j֦oP_g%VMæw7Pݠ(,mIa {<4\+:|¸&n#-{2A4?^F˾o ّ ,訡whJQK"x9sg() =ƶ-@E3UW [ 'l&y1n*\+'aаBF1Կj{)l8W'nOPeU0(7Wz%՞:0!TjV"Τvhu n#0z[V"1]&5<ļ$06$C?i륿UVh ĩΣ)L9+7xuWfns@F#.r2;ʪ[''$x9phf} "M d:,)Em> eAႵQ :%"S6Z`쪇E&*hOsFV ښ޿pk8S}uڼ-W>>T_&Ps:>$T-ZǴFs'J$КԚ@`h?eXN^\[d NReE`VVd'pUt񖼥Mh.'ʤ.qM"fXI`;rS l](8QMsJ%Y&Aqh*޺*3mzL`V*/c[^.*ʃ12 z?iPSU@ًTXE4!Mb Ng +I:o^Fc!wc F%!NTPGCaktf%xX!lޠ 7Je5X W,)j%$0uDy;n@)i|(TiB(px"l`P1mϿBS7 _ݒj)bao&uQ-0⸇ؤGTG|S S |y3%RTܵ嫖eI/y1C,=ߤ%ȆϫvnvE4)PO_C79ƌ] mTk$]4bH7{-4dl(aN_.n#,p+FZ=)(z|9x-vHi`ƗŒ-W'5J]PŶVsiĐƅejwbW 1,x]ӹ5/gbH@ƺ;*q <5F!_JLCX'H96۶̇VuODpַDo01<?#1kXvWgυHQ"|sD.u<]p4lG5gzUIѩątrq)j<8$s2dD全 18( 3_Fd10!KEn$fŭe9Ar%muscs?;S$X(TՅ$N{IjEd'[uL-Eɀ`0FZ\YЋ@)=1_87>FeHՐ%ٲvfJ9 p}%7{j ޡNywy< O"fF Ā'=/90Ѥ<]Ϋ' D|Bfr펮zwY4XeDA#+m >f&٥XS鏑guntS$ɍjCqs02?Gq`&|`΁eM~<ǼBŏBE^9x5E bGۊڰ6K۶f0@"`¯v1.hm7Gz! D*_Ȯ٤Փhܬ/ ? | Y{K<WB_)ܵEo f # ?w'7.Zv*"z{PANO5 _90GZKV]WUwGgj'[42,V\t%B.R\ztj/͢ߌRLeI(pB40'&>[0WшQ-Z@<<35$wq:Ќh>;w흨̳>=JRߑԿeF{iK]- r w^E8-n'gkB9i.Ƈhk)O g3ɂ'M5<ɒ>PW˶]7C QgDUwuv\Y]dN'fAv&OxJ\)9R؄vOX`_BTITS\ѯPnˌlÃqd|n@cq[a'JsZ@3luDqIJxq^m}!n8XC4l uA(d" 5&g_j&ЛQLW Sya,m_k=漍'- S@uM'GP{¡k1 <?1!K 5N=26(+jv#*Kuio^-0ڃH/"ӐrřH4SҧNe+hiG m?uƑR,vdX;Zj ",[ԧrT*#Q?_;gD8}^ Qݟ40 !Ȑ(6Mo}zx%@"sMkV¾UV}Ga& uzk yoȍ1‘Q-}"7Q8ܯ>'(*ɍZ[xI_Xc~c)o3WenMWaԳAʨ7*X.,io TɃ0({ڃ9B5.1k(_M=YVַJ 5Jb*, y x kBU0PˮR>Sh$U$՘cWi4wDa+?sNo>pc?Fc;U"%Cdn4Z3S ]bRz< 8݁䄨iTQe?]\~sNOuPm/EC6~W]Bax;.k0ߓm%AAh<6DQA.!ߣ/͍ ǻ7uePz=^ËĀ7m|Ya4RC4J3!*4Dt%v b  <j>aZPV?’E M?Sr?Z 4wZ B,;D}P'M,Ѳ m-wѹ% NR"ɪQB0okEӢO4iO1&A8a5plxx@8]P%!h0: %x/C+94 #eT<1"n%)* EXO1#9cӼc \n]z=>x8x6޿<YsdU=7^xg>p3b9a!d5.nմ%>N` %#j g@T*Lrv1 $avѭb0.vSp*JhgZ LOvvx,Tl OVZkؙr _nX(YI*0"ڲ11|: {m7rRSsh|mh`vv؋T1 ,՝6!)x8NZ%N% s`&w eC4p6!khH UV5ij/^xCkaay_|+ O8ᗕo~Dx'pdEm22{jQ#&11ş(BTZI =N8p"r|XvpURUBI{aNXEӠ8LU}PԐ Dlɂ #qyKZPkЖ&mFi&=3MMmsРm2h6;x1Ζ5H>]zJUᱝ'h饣ͳA dAu0vi a.c3Zx?H u2'ŗxՀ=vȆ6NOGOޢj_m%rrkg.( DDS% QeԷMXɣ5S @:/@8Ww?{'O|(tDh{0rMGs~ʨC?\ bExǻ0I|nNInE;-QZ&.c!՛޿?+V^hk0_VAJ TNUŭԝcr3^ӮYp2-s&NpNA`a="<mwZ`]羃pwYYeN>oF{l0bwҋ=l:!hE ,L p#Zs.LcOIĠ5L `8zoT{ěj%br% &nݟ bxk%E4NQp?;\_3A]Gʓx-̮Щ6\}R~j>0# f~1[!faleƫQX&G %\2acub)py\Q*2)1p͜qW@Bu-=P<$AʬW0j@nWȳo6L :Fh;1lԀ񸉂qnjNPޮ[Ĩw{GiȇW )EqUMnjr?-څӟ'1fΆBs䭝saU2[?w}9!~e~؅Efׄ Ir-K/TN m@]dN?ۓ;))8=2w֌Xk͚#*yp~" ;p'+a\ ,y9k9gB>z'AP uA - q#LhZۿa~W=7ߐ!۠N1U |nd ǟ3?m&3`vXIe< 5z wh1*'aAxcܱĨGy(cpUٱ"ZU,uhVW@e;(m ݻL#j% &!4d"#űks}fkx+Bw\40nќk鸘'bopђVN>rSO#[bQ(_~lyQ&n@F9^ xo^up'~}bs@=܋3#XSsXP42Nd=ʚ,|@nA2KgB82{i92kk`|?N,7M4SGo WofZv=) T޾$VtP N0UA DI2@R5p%}{S~+ +Vֵ w5ޥأCWo JvL)RQHW;-.9@g_,|#1Qd8QF!n~J(g0OcJhjbOv~ ݣW/C>-ԓ!9DcV]t[+֘޻٢E_ե(ua+ۚPWȫ߳L`y;WQ]r|qָaVE>}8GZGXl(f@>Oohe3imգ/mٔX6ITHKQ4Xr.p Zt>M8׎ޯ(;QD]ӧ-޽uw톨o%/Y5FtU Dt~0X/3I6ٌ6I,  NNzvh9kU~0oXD@s"#RpoFs4X&TtWL'8 KCe#Y-Hh<邅 ?x'dwh0oS׃?_ g sC`eŦ; )Km.m9YZ7/\+xoB .mqKr? ڙ!1i,ϋ%`XK_#YAJGaEb-ۙN kz}ǰA{2G7u;#ZX*~O& Ys[GUcH`d./5lz͕(tߧ 2oAFNհsMgc@Kگ@ V[O')gyd:,uҡnr{,ՖKI_R*GOqp04aE\G'd&(6N- }IwB0  Ajlϓ4.av ݸȜk6CN#<`p6)ZԻv34j7JJ1Iɝ54LYc+1j(©o=.灬 L?4PR`=i(bAϢj,Y. -fR3k6-?2Ff0%m$^yfQ҆ V).eXG{L; gZ^8 wdJKX?Aڍ0%i](XW6o$YZqTY̤ O,:׸oҒ3K"SY:x3`3t=q! O`SH#Au w\ɲ, RGt,zڞ![~[ݪCPC&vZJ1P7<)ģ Bn>o"rS3z?/qb oLut_+- <Cn7Gnd5Df <<=s4a5Iܤ#*>jy _)=XL }bVv&05Lu9)wy;NUKccl6 yW*X*yX{kM`&#[N'hΥ@$Ґ'vFE/w+zi^zX6,ό l /~f75׈!> PoGQv=k#6qYR#"l3?)jOjIj`RכpQXTOScE<^@˷ # J-0`!6J3n`DkAx=0 IR39ر-,0 'Y0o r]i(q8\#Y4x;bU驥0H!*Np|ZxR閰IUq ݯ=Kűkx("/ Fjs_)m~Њea| K, ԂIHa14%jI'ټR0ZpE1_/V >ؤ?'zN1*Yej[*p|q<\vm֗\sGݧs!S\)BUnD}^ttL$*LJ 1wpC<5NQXu]Vh (Qfp14& \}4KEGq=.(3\ܿ2o#e] eu zhBBE&@Q-mJ ^Yfe qQuP 0C%I(16.58<D@ 9aahF5ס7D9(x FWSTqd\E&F Uڐ"+I jWs\5nn$uթM2_qYd0%l<'ikܻ>.>ܬ6"Ņ70+K^?7?Z4욓r%P c7n›un) #g$Nx} XpZtS> <#%Id.ULWC;'(S@(xhŹ,KQ1<:^2 YH 6I`uo$@gP Yt~̫*GroޤW},^=% +IkOnz9UJCqʹS,, iӬ<+@%X^arp[ MnU#^e|s"Kx+~0 DQTVl3\cE1ճ6@yQ0 {8Z6Ct6@`υ \L;!-B_ KN|ߏtIU 4WU_iMV$ԍpvՓ1Xa~ @<Р@-Cyqg%Ca$ai˥xi`TE7[>y_gm S7?S'gx)Vo3Prsr1?a,Ue!I>8^gN:!#r BTЙbXZb"UX,1Z$2b,&vXT"9~˕kwu0t p}qqWY܃\96 ] sQROSR׷:bF,Z %eXm!;j*1+m8gMBQ{s>2gd,_;Glh#Ip #]u..VMSmZ$Wˣfe)KFnݹxp!Vd}N.gwM{OOȾɾ#k=`TސAˣ MlgU3*m(ƎL"շ{GG{w<>9%ߌXEI $~԰3K%m(c2}9C7&%F#MIe9)uI=ENhXv67 >fQ9{mVhMVNfcK=8;QSБ0?qЬ[1Pxo75p8LTqh4(3SD"=E w[A\-oYKUP$Ţ+&Py8>QRb}GPURg>$4%z{|W*A806EG(SXQ*Z rҀSL-Z2PJn 9 ~v1ؽ36+o)JhtbyF mo9;d| c7ยM)hwSYvk., N_+*CyhϬ~ TmH䎢(ՑqqLJQ|cys2G3@ # PE*Zvz ŎjlDGg}w Q==-YYGbc+e s¹p"ؒp>@vov!J78I`lBڅd_we PQhcHE&E۬/Xr?_ !F!'w]E% Cdl* C_\xFǰN޿.9?#Dɰ[l##l:T2;`Nhy){1V/8\EH@)neY $" ˇCrLD22 oRz% ES%Ȉ7wwa T#\bشFTf:/-&Xkdjmiry>V7}G K!dNFG2S76 lzn @{]4!vgrx5)|*8J!l u)(`{O3*` aK3V x0mfHh hօԅR}l:wiW# );rTF}LPy L츷0nH~|9I'\eb7i0fn!S*aQSPǮ#ՙÖ wjpjJu5p"z>wdrkݮHu}Cw?h mL1:i;'Jg̽8@uA[LV={Y1fffS4Z.:TטD XLH'$=\'!t4zA& hDƱ ΅1̐AL)WIz<fXgLnƣX`Eegė.` UlǮű(_(rI6jq )UHM]iWVNX7J XXR4C׼g$FŹS(za]42BT,a9M!w XШ4m5Kqd =0m l9C נUp'bl'!DRAe8-qr&{S{XPS\w8#F mW2*J}X36"lh>_$HmSS\5 OLN_Ծ-T[/_!M7Ɩi3~1SaS /p`MYFIQPsmҷ’% IǓ``TvD(}lL SXxpǸդ cȒp(ANzK%l2!PN@,}ix,cfGy~NݝLMr;lx/U-׋Y =Jw-2O" xxY$`Faf# bJ-dx~'Ix|p1˝5[_BDx=f$"f}xCk7<Vee^$-'qpPULvq"e%[fz!c 6IӫY{IJ$u4R/p|yT>9dPt|J'8J-ע Nⴛb~՘y%N:tW-ί{dB-+5_㩏BOZne"n  'JhNz;"dX$Z?'#^Z Ջq4:QKmd*ob܀ Y%1;vK2]!᭎?IfdNZ 9 (2s46q,LC8^j᠒KX$롋Y($? %rR. (@t(FPruol1JF^j߶% gN#3z{Ͷ:qk`JI8TRn Ly t2جAPԤ{@WHZ-EGG zIۦcf*IpFәS|C}D%58.%q[@? pT.tv=2iߤy)w)zgr~ܚc/|Uz.ke._m#n^g :_Y[4+L,'<Ҁ xm_AO%FYXdo9Y;u#YV|6m>Yܢ{<%]Bk-:(|(KhI_sokWJRٍp<6QbC%xLKlI!hAH/1 K`~aW-3( $ <ӧ󈽵uQZ,r1(R xAA&!*HͩdnXS$-cI0[+V_S4ؤw-p9.f#q3o!P. <7Rd{K|ŕ,ʃF' - U̲#B{sN)iKw3%VpK*t\QބE״+Q*.NcvC註cGηm,1 R3RػuF3Xq2y!l @F%rj#80:< h[>~:ɬ@PaF8u>}#L'N}4:|/u춙\ a7Jm_ 7Z:ʧKDWb$-g:$ULYve[.%~A&۳)K>.&QuF̍/6iln :H-ro = :&T] d$Je"Z8+froK;b4@`Otp\SFal8,K<=ef'DdҰRnl"q%H9Yj|o):{ӖCL Xv8]1.j X@_!w_6޷OR0>SImBx%rHdz@T~I;N$ worA`jt}s> `[$r@zׯr;\b;G|P:YVI9M=Is&歪m/i D = /#ݣ` cGDZZwZM|&4oGn?8̤UeZ2TJuFQ@!hB"K DhC ?hdv@0H} աpZCbj\>aG&(Zd1eOT/rD#J]QaY@twޭ27 Jbi*wy*^ ցV%mWGb 2O649J$LiEHcl2_}^g rk9=xjs89?? _/a}3PB{Tt,Yz J2-9˛lqQ/e?<>|t7Pa"K= 'Q5j eG1AFddsOl\d+k6ou| U\FnH!6ʴ׮WL'l,8E4CIE&uH $]OX꒏;HV*|/cNL7/b߶';~J|ð_]^$[ӕPτ*txsd7B_Y4l;iM韝AϨݬc09-t{FsӓeȺ8+RA(JI'sn$}$hj^l* PN9*Dh1YOA_n:zL.d_ˇiQ$z 8;aix?}p4<L'hqH@Ʒ~j68C $S>mD4IWђ%aSdr'搾Dwg!J bR}ѧ'I'ץ' f#'v,9Y ڟy@&])eB,1ko{ *Ll)hжfܲ4"Ä'}.:`her ChR~: \U1AYhbC,Җ7n7>:,] >[>N:9WX|p_^n3Wp)^dvUzÿ,YjC*eYk RGۅƉtCW4bEȕibH$V(Ҫ]k&Ѱ Euz)wx]!f7#n&r>&$xqspxr<rOjOv_CZ/3 +u 9 ߶PڮrdL]s1(O8c2<vjQ㚉 -j,=]u~d.IAY,'Ly2Gs7ikVcN /!x@2Po}A([Cw6 :O_K_kAd!S/}*[gKbb褨ۨpjA|A%Ы("xx?O1ї0bJYtܥTMv{RߑPٺ-.U(U%oV?p+\QZ7wa\wQx‹ϥEo O#DM x xʹ)a.3 tfX'q_r.d9L]f:ue݋;WY[ǜ_Z ӫ'Fd#c 2,O|]ce *݉/Ҥ,{9꛴昤h91 G'Ê9ctgǘ9c/o A/Y1)24aYU=~|N۝aQMkYF\ҫBI$<D Itkk2. x!i7aK I mgC|Q]ƀ 0Ų cp1xR (5cEsPUSM"1Ea~?ΰxԀ6sd_ l޺_l4[)l?Ρ9iUVv;a=yJB枥zP[B\hN~lfVʟ G?"l,yi1&>p&|z3,FQst3c.( ?]u3L]f058N^$j[tߊ{Y^Bsamf_f~ dP1.0Ze8I12h\.`qfrgMWkj+&u_Dpxp*dqKM:Y¢oFb:~ KF m`0SكIAX85chHs-Uέ%QD+޿]88 $6H:  TB۬3,oMg0VZSx"^#lM&d`m^Dq΋JL~ɵ̽.g2 Z/&J-8;x㤍0FVa_ԙ{TvaiKM3ay7)}m ?s7|N-nvQH.r䆷_eFQ=tkkSyx\&?%SA8֠\0a vswnM'y(VqwͬnӸHի>h1^mQ9w|Nhj:-Gd;tę&qDWB"Bb%zG0A tr4kl]D |탶=.ZARI ;6qh/b2E'bRCAs[~Wk=ӒLPDt3h N*jGWkhc+?&cVd-2JD= IY6~VN2+Lq1?-&nYɇ:L=^4n*UN1R"2$݂nܣ] jL-&&HC^2Do/n[zY2Xgجs*$`:^(-1|S IH:?k9:}:%dN{pK߶{zϫ/r4{셂nA8ONd1@q %OK9:$-l= Wq{Xw#g$K\9-.Kf|J{UJ5T n޾͹(2;vٍii$rܛdBk(L,e>@ Xvgn!£Nuܸsf@RBC5H_p!4ix{Ylň'A$zxtdsi}_iZS*B$')kSohR4}hܜc(Dby҈+Տ)HZw.Lx[ Lf3xeroVQ4,[;N V lX8p|p,.YWwT~c3heODf*=,WRN@fOMTy^Vb\7+8B#$~0"͒+*ȥM!-1a&1Kf/$2LoUiZ;. `Yd:-!J#r!<+hke.츜 q- ᆲF=t8P&RlF7OOX \i!kذ}xEĴ}eܶ)"w=>C?`RU&t'cJT1?Vk oDr=%wbtgsĞŕm]`pvƼR(yv_@ojz(I]MG/…,mȞM-=5ֽ{O&;.*fsbVNm\Mf|٬ɔenp^LX]vCLf7R: "Ԙl汑n |Octoο*3# S`QA9v8&ofQ.(aJg ^NvG'u~}ۤ6fi5t< q%4T>AM5 7To;_:(%0+*}㩪)\[z*>jrJHB&KO%Iݙuj˜E+4D:X@wh 7$ukEOd04ԍ'se㐘GS%~[#\Z:9V47HϠ s\9j MB}-2n0m76w4{r.O dBoy}p]+nCT$Gv.Y'o3{,׶X t՟?>()D t&z\Ujʁ{QT_[8I`;]MZxJO4 6%Q5N + ~5ʤ 1|6<_< ? M}FSDXY s ^mFFp̔`qA^CBXSu\xFPD>Y, LM>5PĺM-lWr l/W*gEyˊC_ %dDyvywY>+Ǧ'`IL =P4>nl&< {pZC?'Q3W@ٴ'ԦJgvns}[w&!4.fؓ=t@MbT牧nPطbshQ,&a8YNZa{Aמp~y7Y`s A<>Z@0gQw!$PMډ6!W䋂vDQ]hXJ\AR̻=9K$@ z{Ϥ#s)#f6)[W. Οk!q't ]&{z_c3$Bo JRI\s %( ^*~A:q*M8Gg0EnA aP%zn-WIp -nEV&_=m|qOl׾  t #>zfpԡ.)$CAUd?m7w ˨"f|8mxA TB@E:3TRO+uWt# in+No.HͫIҢ¹%ԪA8J& ?b~+"O-.J8Қ"si (# X_@%W…8}0rAw'wk9zj0W<8_P]kv$uuی_^[N-e r21i Pa+|{,#ڼC[88Ֆ--Pw 0O"u>(kq6L{ WgZwtN=(m\%4Oj\hε?=|4gsS t0ğ l5GBũviek ־.r5`ʾNf !d)S3)L%q:[9TKs`R4>Ƽqpwx ^x4xɰ P4 'ϓ}^t{#_QegP!bZeZpydܰx>)*&UU&"[Űb;Pwa2RL#%Zt2ZfJHOm&S's(_nY@̜ hbA+⣂($G#"%S )!L2vg?~}m:JǓ,bpq%^'DRåefVm8*NK?OKB ^JixJ 7Ar{#7)גBL$B5,)=43kÏtbXHٔKʱ_LvRƷLxNLnC '* q4o|//`*}|1PcӚ.Rlڊ3A8Lbзx?)0J$^8IIh,7&&d+rYr'~Z:{ct)T{.Cf7/K~0+3XkhT<>@9B髺]Aɒ)޹#w\AkQhxٜ.Po`b[x\v-ck-4mCZU YqFRhA|)gan`QӦgCq2gU_Lk,V/!G#@4{}P>ԽY7Fyi}/wUͅa[iLPHq{W0׃)>o8pc3+IZ\ S $xt}sHA 0gpV?=uW?nygǃjN0`;"Lm[ ;'NiN\WtcB4?G9È^ &I\H҅OXK{]^CCot`SìR wc O6mIl:Y5y~ P~.ϪtL%cj׌׃7&Qvf]Cs TW LY&YZ#{6ltXMaR&,Br;8=<:9}8gYG눦>⾐ho9u[Es̓V_/I3 kG~V9:k|1iA[f6Bu))x"lWvX =C VjA u7̈}࠱jv֤A뗁熕B&#1,EH.NoYxGzMNFTɤ/i1ܳQ~hE'sqhF02 WiaRpJm׏ipbۙ/0}O0՗tw.% 5^4FDNONL8\ х?C_u8$_-< ]~OE` K"Zx!lX0ϫ6 |x,XW?޺;GX~ Lϛz!kXq)c=灩]8wxuz,^^p ez ff٠ܟxA⸂ g5<<6tiOx_v_c LԜ =zqOχG&m|>cf ͻy29V,p,a6@#P7qKY0? UWx0U6ZV8A8]ڮ]|1>h=W7>v.sa}pLp9׳/ĭrrD-bcaTp㥞6QsXh[+iQ.zQfo@NwxSު; xQ/.1<B/k>y75޴ȋmUƸ*xWs0#E(, #;pAJ̗}RU%U`;ȵ~¾IA''ߓLp~YLS* [% R񇤟3,~x_VFh8djYvr`qXf@Xq?d+:}rx_TUfb㸸xX'aYj[NJ?>Gf 8>vxc)`, >c]"SKвÿŬ!rmFf ,Yp7Rm@ph[M> mg& ^b"p|a6ba\cD%*F{/߲^daV]ME/HJKe>9y=E1/Ngx ϋ(r/.G`&"=&!ykhGߪɣ#cvĵY/8َN2sPYA1-”Qjv킲/]FA ЍŸ`JgĖ/<[, y`ᛅ[Ԩ64rכW YUa[<|( (; l5c7(tdvh!QŰ %G颞 ,<cY3'(NðDY|RwpIeahmgSOרڴ|EiNF![roxo$h/_14x}&eZIF6`[Etnm$% Z68ѨRj xꯓ.F4`rY&ς12l Ek2|L^ m\=5X6Dr:g#$ekϗ$C 䬐qѢ?NJӽo}wKTѬ7Yr [Zfrd⬇H̪(>Y0.E㤕G6sR2;՛$RlOZԹ6$ZƳ'DUcAUn1-Q%=o'"դAy,Kg[pɎaJjy/{O=Xmל\p%{bJ&\جY>ƌiJ`Nl0_bkNa؇ , HeW1nq[PuDZ|5,_P\BUPv.qo_rD&(`k6Ѻs)9"}M < `p'Otxw6  pbϛ#m#oA,Vq!=Zbb4l|ܥ(R`bޟ;Er lff'|M& haUK\vƚ_W6ׯ7o>(zGN2䑷ȋǵT?%4* U_ۑW>QNPSAjKL]4=ΦvZ3"D܂tNֽgrx`IΜHtX틑D: ŞȦl`^y?,7=#BI0ASQ'_ I="vWRW"Ӗݮ05Q"7*{qKD~]Y*ۄ_?Fs1ͬjlٰgfS $cp`s b)A^ӆpsEC&@QPL0@昹Ĭ9:{t]ٲF`}0`x՚ EG)O;߈٪AXl^TV5׆1n2y$Kĕt4x@õ~_ ^B7a%_٬!c>I&}x(]>t ;vzXX'sÙXHv(2u&U>g= m0@M:_SjQ݌4+gR#m*rڲ/hf"9\W 0l@4t́$ks6MݝW(qn`$]IyE[Rp(yLhKS69:|>2 0-ro+{B\m^f&s$n1ƱE{ T}'ȓYUh.2qgQ2Ao.Ԁ+9 ; q)|Lh'aj8lNLx]ԐR"O4]+_Nj~)2TъS+BoőA FYK{Z}*]#jB)7h=80P%2- 3>cVz󀋾~=+eP46*FB>!fqFl#/_~ɒ_}$?(Cl{Rl~~ kڍKn94\ӰDN F?*͋McKv0r04e&!<_~;h}h HX洹EMHvR5a'HOu}q=u;)Ktj%v/5 f5qlh _K_us)aMWzyiq/s)F"Ϸ V˹~J Ȼ@abe&RH+ J[ cJYL+ukCn_$`c-p.ooܥ `ᶴ?0~egiBIXe7E>>V@C8-{- X[NP"뼻Ea.ӌoxP1.(ƢlI6 M GF~SXd,t["b8` 3t2[I$-(2f'lh4Y؟_\4I9׷z`ue;d'_D=[rȬECEXK#ndzRV䤝,[(S ]eh^a&e]a%=툴xql㈣H*W @FM?`[nь8}#F`+ľ9K)I`Ih{}݃ʬ6k7Fxa6Bx%ha5[dr|yC]xe9k߿67q]՞BTNDy-5X~XkNe5L?@ָ]pƮSn+zC}4'ѤY._|@T\o94a $M6 }C3p I=ksދ/ۧoj]GQ #d:z>Q[(@{Sʦ'5>jnc(# `kĘn|'1ü\;Ka'ᄺ(K= ^M>8r"+Y5RRO%q x8h)m(%sFցL +-[g:Xr=}xrI/7:,H̫&yCtٻsOx@L.(={t@yF5oPa7D)fljr)fO'5O4%mxX4 ]\5yXcsf,ίK+{MJ\`shs u|C' bnhM-xc6b)T{Y+-:ømUW&mca^_x̘[NJĤ/1I:t"ydvLtE6T2XB ъ[<΄E_s"_@}ZjfdsV-!X QjEf4 cmonl*%wOc0D C#h<(>m/W \h0I|"񮽪6.j?7Bz?>% [Yo4K }T8PK@hX.HnI/$; EB)˞eUI@%aW2wuH#rȻ.ECL,6$U^C s~RO\vhT 47gNJtO}B&ӛI^ l;企v8s&u1|F( چ@;m3i0:漩Œ*Xc4\T'b8HE{]`7Ψq`FJX\aCbE2+,|*%k6Ic#t"Gq5]7ϔ̧syQv-C&"(#6 C-JE֙. 'M khmaDHY#NK/ ]\D@o)bŒDj!6fX[1ÝWDmV\aT&_p<:w9/BW&uTHZɈi@ʼU)6a>5YYT.(w9[dVLH]{aW޳|=d0AUݽe4Cp}5ZN*ń5bl,lT򚊋/Ŋo?MX"_,H=1d˧O/ d_gR]v /& _{NŢ?dHZ⯳~ 6<<M?\1L ;}1kE& c@0tWBp, =c1mOV÷ޮCP{}6`y:|_ 46?c@A5D0iQ$#? Ibɝ۸{I R1Q՜'o媃P$JM9HZ^U1 u JP 22IῤI4a l@Vޣ1MPb^tIPf= C:!8>RuJ?_fX[ä {އ$8rT#*ݤժ'3M(6JfbyCU>! w~v;B{s F~bȖ4 [Q-=i*vbISQdg˂t1ԋS]/{៸3v P{ocJwa"D&ut֜k‘T1Ҵ`\u'/_3R[s^+ۗ'4wV=U^^f]wWI\bE#[1"+R P!@tAv0h5F~xB+^l[$Dv@!Ӯ}z;"w,ߵA(5.mj'QG6llܘ1ߎ@Fx`)||Cj,ě=Xæ$XgZiV>R7|8jre|^Tڰ NPbrbiTg;4ʆ`oߟzȥm`hE r"x|G2C?Os.*GPϋ "2X&Wd:PatO '\YEو5:%]1,-|>sa 0d2EjdyrLΚ;Byig`gEsϦQ[AP"6 Nn`RE|S}X9lMmpBnb-"Js)$\B| Kr&  a wHC?%v)fXJNE~\2Ǫݤ嵄[!)">CdYuۭ Rtx;g/J%hbEyV;JiǠ(E+rIB 19)k;'' |.%5z]LV w=5Xg+BhVU^WNCdg{ۇڭwހD6:@>VOʚhe-$wwƆUec %Dd#2P*#XKd|Tqn-^#I!ֱZ H? -֌j6d8FCDI-{`(CԊ"J=z,qcnS s/z:B&4M6Y؟Ky:Hϟ5ZGo l Zmk[3. \QrX5׆|3npӌX.ؕ3 W5ޒ&.X$l;wR]~\8 om ̪65La?ƭݚkZ"jm0B}3_GtS#sFu5L5tkԎ:o|{􇗻[;kz=\0PR؁L  (9_Q,0w='@ۢp,tY9l6*'1{/?pnR-&I~8v܎=:q,+ %o :: weI8k~TƲHV{kO; ?g_8*I@&z]ݢirU&mJ@z"w+]߿"ʼvx?pJ?C}US!8^IKzUG_CGoGW  U}< ѿQpa<-~ɽKtقS~BdZ'.v3)* W+Pe 9*Pip>Jx;Hg 5qaMq[9$h) MIOE֑ix#f0@Y 5˝eI|kKK MqM۟zWTHR`m,NQnQ|9b|Ÿ`vd{'L /)k[ß $,RF*)#:Sc1-\kVI©8]uQ +a-hV8, 6kؑ}ʣq$s%A$r, `: x^4ۘʼn=Hrz8)~&uƔrpZZc Bw*|'%BYqaX)7W2,)0%nbi1JB!--WۄzyXd‹+K$9OOd+iIh'Jy^K·Kѣ]+!P8[ELn@Gs GXh+ ~ECp6P9TI`p֒/;u_3p11URWl%+b (d˦(_Ios> ABM eo3<ʏxeVtFF&rn 1 g)取زno ()4wn|`fs;d]-h?{'dsH ;cTu& yy}MlT5_%*ڝ6M{/}؛D!llxC']Q yr7HGJXRZr+>m 9\<`;V N9<ڠI!GSvx|r)mb ]J74`T[ߘU`}0bz(mw :uj #ꏓsH1H˟O^| Y$ֶ%Bh+<^}0Q](r\44pEjZm. LU1GPS%*F#iug+V+DLT$#2]} ǡ^|G.BCom}:Gv9\EևO6:ZT_99 9U GCNjDs]־4z( qHed")N1£L*5 >@̦ym@[@bU%c?:0渖W *AoNᅥfa,r™_f%pVwKɟ`tQ[aSiKRye@K9wT*HKG_k힤mcwհǎ`{gij;QQS& N!sH Wi:23PS`6Hp3|ƣqqף 9AePP.P E)/z2H }Aa+Wۦdv.DpJ%YgW_+g]zO/ʾ M; Ǎq1[h1>9Ɠ/6ͳ.$QÔ|jl1ըŊ#\$z2 l Kt y ݢPIgd(ṚO&v͋@Te \98@+9yu Oe=LxԼI|K@d8o!Z㚻;Նcõ 'RU3sdśHzXk}ͳo ;wF舺L~ ӄGTO9$RHܒY6 VɆ*YL~VGiz=<8I:cZJVSE>w&XR\Ea ӊH#gG3B@$b|4kBȶ ÏpaҊR7ԘޝB,%6S6"Jd"u)nD߳`c.p뜰Eˎ+73)*exWh#2[*u B;DptlW|@50KȄRV" c&eюrRCE&ɄZ8I.E njtY)һPDo_;ՏL^`EGnC4zDzKWDl{4f13[-"9`FGI{ Q*ނ`2I&M~P/..0\p'wUx41[}@6I @/aȴxBPD"Z6N j4v$[V3U: jv3I̬I-m-nA%~QE檢FX'( NbfիPMx6U|}"I ڽ e*EQA]~T#KTRRr1Iq9|AR/̝c~̗kX^o=(~ c| q|4D}YtMhcY"&p%f)TRH]0J\^& ^7n,CR;Z&ZP2 jU/̓Vt;E}Utf m0 $Kiۄ u(Tpµu$Ź(Μ)NZȀ QoP7+-,s T{/Dr]l N++mE )z? 0q\qY`v9u"7|:1E;qg">$2=|@KS{b'-_cP嫣F<3Jy@fh Z⛊+]Fqw,VQP#QVl3rG& GhY5I5^@]u raO1Ýg϶r* DT ͸J>gd`m(17ru8ke&Ӷ_piȉ!``GRdc9U$3"BdL [ rGZkƹaܐZ"|p V1U  IE|&z[1 {?˿VĻ|{YuEò{8z0C`T@z;G BODηY!lo%+^fo!UqЀfH\T"@VxGJt?WD,Iվ$rq0e:jI,qij5%N"g%MAm &N'b)-o?Ȧ$+uR`ll#mC{~/?>?QٝGK8~{'sFWKPņCй ]LB2mxUvIUWV͈ m6ʻwL(y{p^LyȜq8^FI@CQ#Uאi`Fs:"iݛ BANjE4 qeM`|smkәR G-ܰCdk7i54vnHl}2zz18)/䍮‰/¶ /$F4\aI8) s6K7 lt=! ivvs59~d8=Cvw k_~xS$uyfj Sv1zobi*!1s4sY9Ey} D w`~(+σ/֠Uum{#vVއX8H:}wpt|}\`Q  "I бA:4wɅq'kO ~8o/CqL{ATv++"HC߳Y;Z<|rk$jͣb8?1ZXջ5֣[symȪٔI!:rb~}acb`-pe~s/lEz.n6hpJ350]-(ޥcgW(?TT%. . C#Mgצ{̞DNtre/B a+)V޻f΁#k;..bVLBQ]hE"ң,$$ K bjӬ9;2u9{@%O>LhkRdVx ?q{tլId6G]Q\̑j-ԢǤ]H^] ]r. { l]/eζjvHmSA| *xwdQ *cDj,p:]=xI84酔_ (NLܝnpv+%\P0íO%GvMwE81|4B͈.,49Ջ-x]O~=;~Cvy=*+*9p5@m[{!2ƛ,#-b*ȩ85\v7dkq! G l J0đDf'>DQGRjv[),T%w&S8o5h R$I?n8P2]:AePQT ~W* I\j&d3ʥ7ղ?YxlVdI=eLRvџcyHN'!Ř~3H)ET3ujƎ; ~@)>?nOW,*LOXddö`>b2mx0.NVO |[q4m\G^LI A m* a]Gv1.E%N7ْ 4:I'5Cs]E=Tpu yW0wB ו?o(^ס 8,ogc }Bjf@, {|6 詊 H;YLmKVHO5W K+C}&!JA_9ڄv=tFTQ|83#ϫvn {´(jqvƢKQ72(바c8 ]C! K@nG5@YŰ 4 qSWMF" L![ukhѨnӯa>kۥkk'eP4"AB$\tX<44GI\ hj7MtT (G("4ipZ%uӹ$j\P+D3W];'3u=)B[8MTԮO.YIa{)ӉtO;O8!*6K 9FÑ2{Qay6j} |ܲYi7w!q^ܑbviN%jx'F}Yˠ 5R%nŮ!#[͉KjY{'< $k6S4k;r\W>F]7x >ܑ~fkU^uY.Zs!eXcK \n[RUXINؠ 398[=F $NP;Jo8!~[!v\LԪ7T>^ۀ;R3;2F%|FK["'uړI/C(Gqv_IO\ fKT!犔Ly/Q\+Ppldo./ R` !J;) NJ L{{mvU_M;J 9|߷X$1Q&O$O$g4*崂#QBvն(>@)f˜AB[Y8#"^a5FJT 4/` RS;&$qMc3룚@#߁2Wy8Ӕ~u4 ۔$\d̉Juj0gEyע=%Hl58_ ͕W U~M v+io3бlp2֝lT`,!B*jo¬Gwt]n䝍$!=.f? H_W$ij1;9YU:[V'pn?}kRS^.2-'y#fNF\U\-$X; Fi,DƟHn>&AZw;p @Bhwvk=j*'.΁ٓ 33 CG_F(2ϙxp)4Fg%QOrˎq15C U )1@TFOsl64H;HD[ȼ4R-0¢3Jfp) 6Km "2-!ءۧ!|]kţQ(D}&bBO&FIa&~iok0`g[[nxwm>RU$;j$DwEK _`rk8$GfaBqɎd9Ѕ]ɎF0/ n(`" &xȎB:OKR-^@{ n8< Τ)1YbFds M%R9aSg7Mq~ƶfvw'o؟J&JsQP ;,ƶ2Z%DxtAns!In "N[~6~ nR%7U@]HoZXG!GasVO! vI`Ĕ$f!(Pq~)+(Z$ UǦ>0jR `, ^tIY`u[_bHX! "g^u]~F7>'3 /1|QfM՞{uU_k2,P،9a3 ^6ibՄ9WCȱFck0jΞǯOn 3,&!_t5#@NВ29/jчka@Z[lVvt0Û6y0p s(5Q0\&:,ʢ/\lU9]( |BX5#X`bYJUHP*md4aupL(L;ҫ 247E{OSr4 -3xZ fS_ #) 8c]YB[jřf'+voǽMҬ-|X\T&9@1Dژ-VgYSpI+q;@IڨECOFjv91"GcE.[Bb!0*-X*Lt}qBysГ-HOԠ~k7[갮{YYpL7d m]xmf(^PiHqimhe;~(lMDLRK/F=.UIQ;~~$2u g= [z#E]V\U^3(Wҷ]?l0'L_|̕20Csj`5q$W  o+'ݢp EYUE/.u~XUI=P6q07͈ e!!d쉷I X=6]/ l 4"R=!BP75iLDH@҇ 4,pa;8U{@"F.5A WC C< (" 2ef#k(?h[ 5'R kRz/kyDL+ZwFy %{Ғ)%{ː]d'ۆ]~D]HˡPwh1Tc;>{A"eAڗ&n*Nr;ec/lm{t"c1$bw@0zaqP,u`g5'+.GS:b#TQ;Nﶟp/1}rB(E589- n]s"uħ]r!"51DQHRJ+_E'C4 v9{?f5 x{S17CRhHm$Ʌjbw_u]u)rCOhf877J(q:.ع&_TZRW5L! \pըѭEdJhs@N(X!OW]צ:$uB d(Eࣂ>~ٛt&I)@[z\ܚ;d4] 鷀Mm!01*@_YP1@Vas]_5ܘݐ9jD%bdZI}}-C[e$wM(p}&9 ?** \R!x[AҠ:l' &&E W}Dڝijd ~ ~#_ou^f%} MH X99+̔N} 9DkH)@͒vjэ lʊ[.nt`{8tL>YwL>FDŽ/<&BWJw/_MU̺bDw '%^x^,n>qg>ETv\g~^7n4D zeu/޲5st$kFSZ\]+yWBu #V*7`^ [ Ոv9=EX'x)kkݥoW XgF#yq1{QD# gG:AX27يeK`~+~&zYY?xWw\S^7`(78 m Cb>"6= ?찋q8-Z!cN('c^@Cq~w_\xkFjA[Vq U|tYY~9{i$Ǘw9zǾ8~F% U4`8ӕ9Iqa> j'0z}w7~ MJ! Y\U W gPr!swT8^F^*xu\!ڮ[-$[6'pDkJ{xjCU!Ycך`^:G=f2 6Ov ?u|R+1?Ҧf-c`9¨b)ƪؙ*mΜ푼whF'kb%!?kf 0Jw_S6m`S@>a9~csƓ5u.i \^c B<⬻\}h6#ԅ`D=tgv.,Q;}Dj:hPs_(/@)ɕdEWW1mw,sxץ_!â(mZq]wAtJPm3 tUS2Xg攸) /geT_'^n;w`;ݻɶn ((Qxu],GX0y2*AG.T=^rDe)3YSELΌDAEskIbU# .rLIZ62-Cw2_lCʊ,_0)C kP%Ñ̜֋&m 64s+&P_Y%v%e໰->^ u>A:TOsgf|enLMYU/M۴([mL)J E@I[ee?49&2S,W&RUv(05u=˧ޑ#CIWlz 5f|'ճw=>JȌybXtΪo :o7WBץH+(z m~)KGSq>7>U±zvHE(o!t8q65&[Oς{ :b-%:i ϗ&lV yǢ̛!qy7/vTw_m km gѵ/,-xmjw<;z<`nTh B]NbnE JQ?ߠVqOs:4i/2 M~r^mM_Nz-8%dIl.[#SF5<Ê5*HFU~v@Df7iMz@8U@C|uʑdIkS6kG)"X7Q`O.pu4 B`K#_ 5 % .O/`'{=32Qi,(dzj7D:$s Vi􈪯`um#K,Ag?v4ܨoS4w~|F zY _> wld{L^$@ϐ2?{~-vsh=Cn KG>;9rE%0*6}m䜾%(5dJ=Hjbv;pxKev j]Vy L<ºEb8. xK';xK+ ށ-Fzkpc-Z9W_}:&cQ DAN(Kđ,BVh|dQ@x|sh#hB7[>WD5i f~m rQlen6Rlh#ᫌ7w ;%fQp30ȫPe*DW͆?NoPDFE[P(R+ KռBK4bvT.}۾Ӈ>L(nYsMЫo)P2!qՕc.,?]_!HdW>kW!qJPne1 ZbtV9+Y \1V>eKa9iʿn\fMpA{Y?,É];b[r{ ru࠘NX$aep=#I_`PM;i _@h%fv$/k8J(0 GB:XBTbnBpo01}~J S'c⚉s,!e0gqUGw:#A/ͭ.W&ޮE?#>Ө(7>00UQm{YfRSaPNPIY 4+(ZMo]?ȿXLo0nJS# X?Ċkoe*6 gs`FLPsU_s-6=\gkO#ʳUMԺ"|&RJn:bS z)_fNhsLZGd} ZV6˟~s삯>>SɇX6G񕺇&J P )DyzrW,BD~΂ 4a η!0X8( kJ>Vep"+(h 55R썸DBH0y!Ԓ \jZ\ZruZ7IΊ)sCpur6d+E_'r#F_FT ҊկPOéθ҆hd6(~:;wpH-c`p3a+Dj/FL{ERU23= k Q䳙\2jh~|`Ձ.3֊'Tˆ[Lk0Fi׸ N=aejˠ2: d L[ =uȑv43آ 84śU=8IU0OfѶ;]pq֨yhD'g$C`@`UfA9Gۤ]LVdorW--iBn2NIasFPSP[Yhv0,|P=)('`gմ} џ ԜS}T@EC L.%N#V6m#ެzgFPttUkMTo%, EF%E eYPX a6l?Jas.m!icGخqvZmc--!HWïU.cE{k>[k֟LFvIydXK }fYݯ>pq*_b;>UB3mjuD˛T_}UQ(I gEa/B2ipy/>wC؇$u<)΂@-1&R#/>fe][GltT8ϾY}e8LQOe/(o !ՄY|ym/e`1 I q)(rd1޹-EfόW;Გ,lބKn !JD@ŖW4*`GS< u.D\zo):M]X vUr!V m.yHUr%tDrRU@XI+Lz^P<ؘUX]_)}Ck.~@=Y M t|4XR`ɉ#yl'JX;J)99qDyٿ 8XK v1m6S=*-AA#x$fq=,Y1-ҷ~;Ǒײ d^E b~o#!J~"H\p2[ǮA. WD@Sybw#c΂ُ+7{@,x䓀G\ܲBm߲ARh0oo~v4E ?N"/naQ⪹^X+)4w$qZaA47zeQQpo!QԤ7MLf}vpmw 3 aJlEQO.><#xoh26c$AIGH Kk($twrVE]ےAHJoiJC`h+Ԛխ' 2j%N[vPϣI(I5d)(7[>Au^2;VG6~G"A[` rì Z!ߣSDý/CbtEY/SMzpnі9kkbj+t]V^n(.F͌(bsD9q VU|QXb@)`b>y2$e\|;s?' gJ (G=a_6_hsܥG+0*Υ*ڇRoL\O|&V/;|őhŤ",_#`[Ǻ*$ 'J0B#XJ!oelXJDg-_U>ۓ(&ضtqRwNh(rE ySB%r&gQ8\ٟ$hCD&wF,dzRx@dn 1_4"uʹY\O2[Jguf0$ $F P3 IєF]Ǎx tnL2p}'DSMfPo`T űaۡ {5\3-z 4Z-mKeO.ne?vqrf'g3ÌB P~tH0yqAng+?˪!alM(SUt$Y PrCRoI>CԑW_@t'M%kW#4@*@˙jL7@縫=2Bd.޷5y/N7DCaldrnsF@&Ŏr7]y/ZH``l"/Gtud(g+/M- 3.AXdU%H?wbuxqbM6 G?]6NP#녎 ?&6UhRO!Lt~:<>zuo60K]9A9褓ME.}-?8GPe"{T+8xYyQiܖ(0pNl^""Rm~V.b$C6ƾ)3d70y!>y^LϪ$p HGSYEh/X\8jaC:DWEWD lg |E(Se^8|1FPaԯĆ_/OqzU=}BXe62Z(ҔqlwL@u~(v\ACS [Cr3%5>)oUGC)^SG}Pm@x B{2?DLb(RzoLxri;,nK\2,_ja $98 ȉj)"Go!両7b7.~batm2|ǃTQ5ciYaB~.R+z`Au- {qIkp{_-91ָ qTAhx z8X,. zwD-ňDAP 8:ݽ0?lV> L@ttRPY` j9 "*:fL |CE9>eUR,.IΕX7#Zr,?L4DͣT6Uy)j_R;7Yc14AǘkT܇VNwl%O:[D#rH3W q.#S>>(R9Qp {gE[ODY)?FS*=JXeX{j8E͇Qɤ44LD@c$I#1#@ګi Z GFܥaU?/adᦱN5#y@1ˈ=1T|ՃD/O`'Ĭe#n,e9uNP(@&z (1gBjbE1t:t̩49 z %TU{u3P/etrz` 81BFuŒ!=啄Q:Tsqs B՘ i\Xf@8=@þI4V0UQ97659c.@q2?>y(']]R%o {#z5E&f3B|6{ O.Vwx(*1&.URc{pUA#N8NBpmxǹtrpTƭi8 K-ДqH#gFhHe{|I&5~*cfKf?N#y=|/9 v'k_'_pfa 3y7~rsٸ#Z(Qt#eqDX.5V"hCmsM$gw=-m4u6Yg:'gA'zq4v^ַ['lQhiKi3yD4 a`W\~_AUZ{0Ȼ pBPoTo㗭RQ;숰q`o=X~q~wmoFW6Qo׷$u3blӷ ԕl䭑69a.ƶ.gi(zA\;),5lQ;]f?>;8:{o+yyt|1GL⻓~e$7z.:, vW#'TWw3&uҵD}'`p9*6Mx0#a}7Fن%qi-u4N@M7 l7"$U 1YpV dUњaW2;HAhj(5>PKV'`(;^lx]R(d$>TVtZ_6`dқp4qw|cJ_Y?M0t{USFMt E`_. q'QSVx>̳A&qu܂h'wZKTQG*6}X B=} 39b79y`t('mHrkopcJ sȳrI{e H8.lAR2 DA9= ZeBj@bχ5ǯԛA9XmHEaS6c𥬺@510:ڊZx1OЧ]H*-n.xGB!Hu >Nk#KR];B [/ mH;(ᐪ={/^rvࣃ}$s})L23Sçme)4Ǫx-dDห՜Zm3'áB",S:nQH_20ǖw˲餹У -+82‡[, s|)O5`ausw-y3%{'!s{- MuNkso O fnGGt<;9FE/8H]ÜF= mN?X<ݧf1^WG~Z]iZNaL.OVZ\4gB#sU`W_:UOv6UϞ>U/v~ՏG\yT܁ly9i|+0<8Dl]|7_^codo&?c_G< =1ς`$ߧ]A cNg-FCAa H1JZQu+DT:M78N$2DWr"xz A9.hIc*zv ^=38}y߾q߻g ҭt"`T݁U |!(Yސl:s9ûmPpgȟD#\Pi .7/^U. f#Wk#IZ&$850Jws~[qR̝#D.-íߜ21bOV Z'K_dMV$fY\%xLz=C.F@oEcS:U0VcCb"?9tN!@ +yhy9h6)pJB 8121cd"tTx_Y5+ҷ#5hdv+c8[+~̷TaGR[ Té^ӑBM\=SFLh0 @qȸK)k8qqPnBd `Vtn,'z.)-  8&% D6*O _͒չ] b / Å+-=HFii.Zt֞j.TYxPop)+UwűQE%H!y{dN &0]͋vV82-p!&$RnGfvipB\{BYW씤pBKasĦ*qZu6۴dacPAlJ k63T`l,f74\>ý)k0Yz}Qa#ILhS[ X|+{%v8G5|\M[AY#("[]NI J{dq#-*ͬ^??&ه$~YgY?/%h.M8"VBE_σb1dHϐjmJ[[*AVH|Bxrll,q_c@:m)5㧪vaS,,j ^@RŤ53K`T#xZl"<Lu`uӟƧ̧h!4͠ !# mri wp`<X`U_xU71vCsCw҈b!NO:?nTR4UJ LXR:n3 w%6YbS#u w(r06*TC5ߗtzKƺ4ķ(E-E{B%!Lf9F81|.S)] @ it]@Iv觵cb+:/J:q9Ww}2 ><%8T8MLaqO=N;A ȏ,4˳Mksٺg* ԒarMdT{gC)p ͅ}lGu)R *C) ~qp.,XSZ틘;ލWn~BnG_`X9 ]Dhë"vDP"rC/1[_zy&N8Xo1qE"rFR3^c|%0ih֎ӱ-I/:l8/R!qDSY8 R5|3 ^M4&QONσds <;:5="ɑYm~:Ӿ,R+a8HfF;%H 4q\l+/F &PBYX8gU+T,5SKL޴"tXyu\qYօ<"%_~~ejs@UؠYB<<~br \zi"9rYފpT$LE9:]c~o\G=X|IXЍ$B.M'k;Jf6`QLOa0olh-fyYxTOW1^<О=8Օ/z%(_p}tNOB063Yr?Wn@)nC<3, L-Jt-u'f%wctiǿ!&gE~ӫ5 o3Nې[i!C3f=M"t"eO`.^dN@1muTL(W .h{;z!\8MS^aIѠ/+P)i -&P/FsY :#t[@5ċkT i,( AgJFb9:|Xny'yvƗߝ A4Ž,^'Lqx@PJ1U7r) mPځ >)YmV]C#el.殈GRHf"|f5XW\,qwloe`T(Y,zE| ~r>cX+l|;~d&,rxU4 m~Bjţ<7p"I. '1pk @zs2f(Z޻\^R8Ii8әbO3!Hfa׳&=0'+|qA%1Wݠ"aR~r rgg:?STH$קd\BQ[UO~a:qDvM&p!cIh)a*6)DX,ԟX96Hi3PnƪdVTXbUxFWVb&*gp|>K`ՙ«8"vb(p}!0*ƋKKıG1beֲV.~wj٨d/')t]#`=c8rdfi:'=ҭL;ao;2avn:t[5HZ&VwinN7}5oTC}tv*PjV7 OQ>d+7n$Mkc({Y촪*~eYT{m2}FZ:zj)C\ԡpF4xV5G!W0^ uM MZ9A8w5G4 ҡ;N%_Ag)-3Z#DVe}TBh/-HE܊I=ohDN2߅>󳨜܊Lr6pBQ pDKdY;i}ߔ.BD0VY&nXMNŀ) ~,! bi)-,|1#ML^3%B >ؒ31Fyՠq3R>hK>siEY t Oq- HO.#jh/ 87Epтl;R'kLәGFԄ9I4A~?t7CWcqbz0+6nN%2/:?;ቬ5zC+↯ vsЬKb4rEQLLb }p24v K& hհjd鹔^!5>&(/ n*9SnHyB(!+2>#CT_%NO-ƲOfT^+ =J1DB- \]ob(F9\z' r ]ƛz 18rSDf-j@ 5rAZtR*hu6]eN#K0ԃLJ+mfiVO֬TJzlUv6Qb̃>֊K4wgӭ#1y+NKpToH { "Wn.tYnl 1܅\Չ) ݶ]RsQ+Qg.>̀8hM5xT^yNIz>CC[`͚r,a<,&,f;Sz_/0>,#iɴ3)LܛBτ𹣇/o1 PLw_a /_Ƣ" kH[uUHmX\$جPP怖՘V:]ye__w3F xY?ٵ,}+7 |q:)o)HŦ F㻿PB%x&b M:&[=i:&_l"2jt Tj|>Kht1B0"x|*PZjr.x5>qҒJ"aC'_l~eP3N wWhB7ZD3KBZD1%.:bH{c[C(GP e0]Wa敥rav"An^UTH m_J!U 2T"S`z;Tnt_cr\) QXo60,ȜYfA -YLA31zFV-pF»Q1Ba)jfaֹ~˕e% T$7ޗq_E,/=(p8Z<-qN3;uH4 @x2ְ1 {1C9f\1{єp5did;_TL7b4y"Ý,3(t$:BYm8##9=`<&]r:oXbҳ=*F8D.]uQ{Q8Hyӑ*ڞ7ᛪq>)vpu)1M$ # 嚰8+3`Nv=yC&8)g/0kS<;#,QBq4DOS\JByDh3E ǯ"\KSB XZd&@!&(WYE)ހ,U9×\'$xжC:D%l*IQhmWd,h*:hb6HzS\ea:0v4(Nϑ$#ȲaB1 RK͸"|ZqKhɖꜥǭlZNtgyI A~-R0<?6%\29ׂ-ZeR/PTcJj kC-JUI]xT]TqP 5zA|o hj;.@x"]h&5wly4G]Υ-' r]!X&S1]jKb~Y[i]`vEf~ ,HS8PDT괲=*8DCbš tII]:WXE'"wRpY֭/h%=F\j9Kʭl.Mj +n{…乶`#*%?E/ ӎrp ڛQ4+;'9A _ (7A K|)O%pOŇ>{V}jދjgOxj&#$Yk™FY\%zE,"M,%&c 3&⁌Ɔ-Vl=;DeY1J߼c$2_K8o)ZMr_¬gdĵZaйmADG8@,,,/\<5 T¹R'# ,;b[_ RD *' ɔd7PHAi9Oy(4\uPI]\8. rkR%X\ x&!*MSsꚉfy56]OaHL7A-*X^:Gu*s[S]{`t<2†K]{,s==R=zPt@6]OZւIq|K푽/c B?_8h?*BTz ,PwVйslC9%)PځSpЯ9 :i. :( <}[={ry.w1Vw w ,uCsaK8YT9Y?v (އ^rV9eUSaXjx,/S"FE׋RusROUꊨ!L`k|`ضmb-q X!g}ѩ=w%*4$;59a=Qa*%8&\9X Иc0 ur_/ f4qwW3.sdSQR*.bʧh\pڳATO e{M?Ȗ>VAwV*VVv 21KQidϒWFP P8Z=d0X2ê/˱laQm fCovܹ@R\^Mk*>ARf[*%Fn,nf>[ [P\ԃk݌g.*R&ϴ oJE8MJ껬͐ltX!DNDp6?hE[ Bgk2Ʀe}H43}uB&kJ, #/w+ڞLwZXEW6OfP*,j֦oP_g%VMæw7Pݠ(,mIa {<4\+:|¸&n#-{2A4?^F˾o ّ ,訡whJQK"x9sg() =ƶ-@E3UW [ 'l&y1n*\+'aаBF1Կj{)l8W'nOPeU0(7Wz%՞:0!TjV"Τvhu n#0z[V"1]&5<ļ$06$C?i륿UVh ĩΣ)L9+7xuWfns@F#.r2;ʪ[''$x9phf} "M d:,)Em> eAႵQ :%"S6Z`쪇E&*hOsFV ښ޿pk8S}uڼ-W>>T_&Ps:>$T-ZǴFs'J$КԚ@`h?eXN^\[d NReE`VVd'pUt񖼥Mh.'ʤ.qM"fXI`;rS l](8QMsJ%Y&Aqh*޺*3mzL`V*/c[^.*ʃ12 z?iPSU@ًTXE4!Mb Ng +I:o^Fc!wc F%!NTPGCaktf%xX!lޠ 7Je5X W,)j%$0uDy;n@)i|(TiB(px"l`P1mϿBS7 _ݒj)bao&uQ-0⸇ؤGTG|S S |y3%RTܵ嫖eI/y1C,=ߤ%ȆϫvnvE4)PO_C79ƌ] mTk$]4bH7{-4dl(aN_.n#,p+FZ=)(z|9x-vHi`ƗŒ-W'5J]PŶVsiĐƅejwbW 1,x]ӹ5/gbH@ƺ;*q <5F!_JLCX'H96۶̇VuODpַDo01<?#1kXvWgυHQ"|sD.u<]p4lG5gzUIѩątrq)j<8$s2dD全 18( 3_Fd10!KEn$fŭe9Ar%muscs?;S$X(TՅ$N{IjEd'[uL-Eɀ`0FZ\YЋ@)=1_87>FeHՐ%ٲvfJ9 p}%7{j ޡNywy< O"fF Ā'=/90Ѥ<]Ϋ' D|Bfr펮zwY4XeDA#+m >f&٥XS鏑guntS$ɍjCqs02?Gq`&|`΁eM~<ǼBŏBE^9x5E bGۊڰ6K۶f0@"`¯v1.hm7Gz! D*_Ȯ٤Փhܬ/ ? | Y{K<WB_)ܵEo f # ?w'7.Zv*"z{PANO5 _90GZKV]WUwGgj'[42,V\t%B.R\ztj/͢ߌRLeI(pB40'&>[0WшQ-Z@<<35$wq:Ќh>;w흨̳>=JRߑԿeF{iK]- r w^E8-n'gkB9i.Ƈhk)O g3ɂ'M5<ɒ>PW˶]7C QgDUwuv\Y]dN'fAv&OxJ\)9R؄vOX`_BTITS\ѯPnˌlÃqd|n@cq[a'JsZ@3luDqIJxq^m}!n8XC4l uA(d" 5&g_j&ЛQLW Sya,m_k=漍'- S@uM'GP{¡k1 <?1!K 5N=26(+jv#*Kuio^-0ڃH/"ӐrřH4SҧNe+hiG m?uƑR,vdX;Zj ",[ԧrT*#Q?_;gD8}^ Qݟ40 !Ȑ(6Mo}zx%@"sMkV¾UV}Ga& uzk yoȍ1‘Q-}"7Q8ܯ>'(*ɍZ[xI_Xc~c)o3WenMWaԳAʨ7*X.,io TɃ0({ڃ9B5.1k(_M=YVַJ 5Jb*, y x kBU0PˮR>Sh$U$՘cWi4wDa+?sNo>pc?Fc;U"%Cdn4Z3S ]bRz< 8݁䄨iTQe?]\~sNOuPm/EC6~W]Bax;.k0ߓm%AAh<6DQA.!ߣ/͍ ǻ7uePz=^ËĀ7m|Ya4RC4J3!*4Dt%v b  <j>aZPV?’E M?Sr?Z 4wZ B,;D}P'M,Ѳ m-wѹ% NR"ɪQB0okEӢO4iO1&A8a5plxx@8]P%!h0: %x/C+94 #eT<1"n%)* EXO1#9cӼc \n]z=>x8x6޿<YsdU=7^xg>p3b9a!d5.nմ%>N` %#j g@T*Lrv1 $avѭb0.vSp*JhgZ LOvvx,Tl OVZkؙr _nX(YI*0"ڲ11|: {m7rRSsh|mh`vv؋T1 ,՝6!)x8NZ%N% s`&w eC4p6!khH UV5ij/^xCkaay_|+ O8ᗕo~Dx'pdEm22{jQ#&11ş(BTZI =N8p"r|XvpURUBI{aNXEӠ8LU}PԐ Dlɂ #qyKZPkЖ&mFi&=3MMmsРm2h6;x1Ζ5H>]zJUᱝ'h饣ͳA dAu0vi a.c3Zx?H u2'ŗxՀ=vȆ6NOGOޢj_m%rrkg.( DDS% QeԷMXɣ5S @:/@8Ww?{'O|(tDh{0rMGs~ʨC?\ bExǻ0I|nNInE;-QZ&.c!՛޿?+V^hk0_VAJ TNUŭԝcr3^ӮYp2-s&NpNA`a="<mwZ`]羃pwYYeN>oF{l0bwҋ=l:!hE ,L p#Zs.LcOIĠ5L `8zoT{ěj%br% &nݟ bxk%E4NQp?;\_3A]Gʓx-̮Щ6\}R~j>0# f~1[!faleƫQX&G %\2acub)py\Q*2)1p͜qW@Bu-=P<$AʬW0j@nWȳo6L :Fh;1lԀ񸉂qnjNPޮ[Ĩw{GiȇW )EqUMnjr?-څӟ'1fΆBs䭝saU2[?w}9!~e~؅Efׄ Ir-K/TN m@]dN?ۓ;))8=2w֌Xk͚#*yp~" ;p'+a\ ,y9k9gB>z'AP uA - q#LhZۿa~W=7ߐ!۠N1U |nd ǟ3?m&3`vXIe< 5z wh1*'aAxcܱĨGy(cpUٱ"ZU,uhVW@e;(m ݻL#j% &!4d"#űks}fkx+Bw\40nќk鸘'bopђVN>rSO#[bQ(_~lyQ&n@F9^ xo^up'~}bs@=܋3#XSsXP42Nd=ʚ,|@nA2KgB82{i92kk`|?N,7M4SGo WofZv=) T޾$VtP N0UA DI2@R5p%}{S~+ +Vֵ w5ޥأCWo JvL)RQHW;-.9@g_,|#1Qd8QF!n~J(g0OcJhjbOv~ ݣW/C>-ԓ!9DcV]t[+֘޻٢E_ե(ua+ۚPWȫ߳L`y;WQ]r|qָaVE>}8GZGXl(f@>Oohe3imգ/mٔX6ITHKQ4Xr.p Zt>M8׎ޯ(;QD]ӧ-޽uw톨o%/Y5FtU Dt~0X/3I6ٌ6I,  NNzvh9kU~0oXD@s"#RpoFs4X&TtWL'8 KCe#Y-Hh<邅 ?x'dwh0oS׃?_ g sC`eŦ; )Km.m9YZ7/\+xoB .mqKr? ڙ!1i,ϋ%`XK_#YAJGaEb-ۙN kz}ǰA{2G7u;#ZX*~O& Ys[GUcH`d./5lz͕(tߧ 2oAFNհsMgc@Kگ@ V[O')gyd:,uҡnr{,ՖKI_R*GOqp04aE\G'd&(6N- }IwB0  Ajlϓ4.av ݸȜk6CN#<`p6)ZԻv34j7JJ1Iɝ54LYc+1j(©o=.灬 L?4PR`=i(bAϢj,Y. -fR3k6-?2Ff0%m$^yfQ҆ V).eXG{L; gZ^8 wdJKX?Aڍ0%i](XW6o$YZqTY̤ O,:׸oҒ3K"SY:x3`3t=q! O`SH#Au w\ɲ, RGt,zڞ![~[ݪCPC&vZJ1P7<)ģ Bn>o"rS3z?/qb oLut_+- <Cn7Gnd5Df <<=s4a5Iܤ#*>jy _)=XL }bVv&05Lu9)wy;NUKccl6 yW*X*yX{kM`&#[N'hΥ@$Ґ'vFE/w+zi^zX6,ό l /~f75׈!> PoGQv=k#6qYR#"l3?)jOjIj`RכpQXTOScE<^@˷ # J-0`!6J3n`DkAx=0 IR39ر-,0 'Y0o r]i(q8\#Y4x;bU驥0H!*Np|ZxR閰IUq ݯ=Kűkx("/ Fjs_)m~Њea| K, ԂIHa14%jI'ټR0ZpE1_/V >ؤ?'zN1*Yej[*p|q<\vm֗\sGݧs!S\)BUnD}^ttL$*LJ 1wpC<5NQXu]Vh (Qfp14& \}4KEGq=.(3\ܿ2o#e] eu zhBBE&@Q-mJ ^Yfe qQuP 0C%I(16.58<D@ 9aahF5ס7D9(x FWSTqd\E&F Uڐ"+I jWs\5nn$uթM2_qYd0%l<'ikܻ>.>ܬ6"Ņ70+K^?7?Z4욓r%P c7n›un) #g$Nx} XpZtS> <#%Id.ULWC;'(S@(xhŹ,KQ1<:^2 YH 6I`uo$@gP Yt~̫*GroޤW},^=% +IkOnz9UJCqʹS,, iӬ<+@%X^arp[ MnU#^e|s"Kx+~0 DQTVl3\cE1ճ6@yQ0 {8Z6Ct6@`υ \L;!-B_ KN|ߏtIU 4WU_iMV$ԍpvՓ1Xa~ @<Р@-Cyqg%Ca$ai˥xi`TE7[>y_gm S7?S'gx)Vo3Prsr1?a,Ue!I>8^gN:!#r BTЙbXZb"UX,1Z$2b,&vXT"9~˕kwu0t p}qqWY܃\96 ] sQROSR׷:bF,Z %eXm!;j*1+m8gMBQ{s>2gd,_;Glh#Ip #]u..VMSmZ$Wˣfe)KFnݹxp!Vd}N.gwM{OOȾɾ#k=`TސAˣ MlgU3*m(ƎL"շ{GG{w<>9%ߌXEI $~԰3K%m(c2}9C7&%F#MIe9)uI=ENhXv67 >fQ9{mVhMVNfcK=8;QSБ0?qЬ[1Pxo75p8LTqh4(3SD"=E w[A\-oYKUP$Ţ+&Py8>QRb}GPURg>$4%z{|W*A806EG(SXQ*Z rҀSL-Z2PJn 9 ~v1ؽ36+o)JhtbyF mo9;d| c7ยM)hwSYvk., N_+*CyhϬ~ TmH䎢(ՑqqLJQ|cys2G3@ # PE*Zvz ŎjlDGg}w Q==-YYGbc+e s¹p"ؒp>@vov!J78I`lBڅd_we PQhcHE&E۬/Xr?_ !F!'w]E% Cdl* C_\xFǰN޿.9?#Dɰ[l##l:T2;`Nhy){1V/8\EH@)neY $" ˇCrLD22 oRz% ES%Ȉ7wwa T#\bشFTf:/-&Xkdjmiry>V7}G K!dNFG2S76 lzn @{]4!vgrx5)|*8J!l u)(`{O3*` aK3V x0mfHh hօԅR}l:wiW# );rTF}LPy L츷0nH~|9I'\eb7i0fn!S*aQSPǮ#ՙÖ wjpjJu5p"z>wdrkݮHu}Cw?h mL1:i;'Jg̽8@uA[LV={Y1fffS4Z.:TטD XLH'$=\'!t4zA& hDƱ ΅1̐AL)WIz<fXgLnƣX`Eegė.` UlǮű(_(rI6jq )UHM]iWVNX7J XXR4C׼g$FŹS(za]42BT,a9M!w XШ4m5Kqd =0m l9C נUp'bl'!DRAe8-qr&{S{XPS\w8#F mW2*J}X36"lh>_$HmSS\5 OLN_Ծ-T[/_!M7Ɩi3~1SaS /p`MYFIQPsmҷ’% IǓ``TvD(}lL SXxpǸդ cȒp(ANzK%l2!PN@,}ix,cfGy~NݝLMr;lx/U-׋Y =Jw-2O" xxY$`Faf# bJ-dx~'Ix|p1˝5[_BDx=f$"f}xCk7<Vee^$-'qpPULvq"e%[fz!c 6IӫY{IJ$u4R/p|yT>9dPt|J'8J-ע Nⴛb~՘y%N:tW-ί{dB-+5_㩏BOZne"n  'JhNz;"dX$Z?'#^Z Ջq4:QKmd*ob܀ Y%1;vK2]!᭎?IfdNZ 9 (2s46q,LC8^j᠒KX$롋Y($? %rR. (@t(FPruol1JF^j߶% gN#3z{Ͷ:qk`JI8TRn Ly t2جAPԤ{@WHZ-EGG zIۦcf*IpFәS|C}D%58.%q[@? pT.tv=2iߤy)w)zgr~ܚc/|Uz.ke._m#n^g :_Y[4+L,'<Ҁ xm_AO%FYXdo9Y;u#YV|6m>Yܢ{<%]Bk-:(|(KhI_sokWJRٍp<6QbC%xLKlI!hAH/1 K`~aW-3( $ <ӧ󈽵uQZ,r1(R xAA&!*HͩdnXS$-cI0[+V_S4ؤw-p9.f#q3o!P. <7Rd{K|ŕ,ʃF' - U̲#B{sN)iKw3%VpK*t\QބE״+Q*.NcvC註cGηm,1 R3RػuF3Xq2y!l @F%rj#80:< h[>~:ɬ@PaF8u>}#L'N}4:|/u춙\ a7Jm_ 7Z:ʧKDWb$-g:$ULYve[.%~A&۳)K>.&QuF̍/6iln :H-ro = :&T] d$Je"Z8+froK;b4@`Otp\SFal8,K<=ef'DdҰRnl"q%H9Yj|o):{ӖCL Xv8]1.j X@_!w_6޷OR0>SImBx%rHdz@T~I;N$ worA`jt}s> `[$r@zׯr;\b;G|P:YVI9M=Is&歪m/i D = /#ݣ` cGDZZwZM|&4oGn?8̤UeZ2TJuFQ@!hB"K DhC ?hdv@0H} աpZCbj\>aG&(Zd1eOT/rD#J]QaY@twޭ27 Jbi*wy*^ ցV%mWGb 2O649J$LiEHcl2_}^g rk9=xjs89?? _/a}3PB{Tt,Yz J2-9˛lqQ/e?<>|t7Pa"K= 'Q5j eG1AFddsOl\d+k6ou| U\FnH!6ʴ׮WL'l,8E4CIE&uH $]OX꒏;HV*|/cNL7/b߶';~J|ð_]^$[ӕPτ*txsd7B_Y4l;iM韝AϨݬc09-t{FsӓeȺ8+RA(JI'sn$}$hj^l* PN9*Dh1YOA_n:zL.d_ˇiQ$z 8;aix?}p4<L'hqH@Ʒ~j68C $S>mD4IWђ%aSdr'搾Dwg!J bR}ѧ'I'ץ' f#'v,9Y ڟy@&])eB,1ko{ *Ll)hжfܲ4"Ä'}.:`her ChR~: \U1AYhbC,Җ7n7>:,] >[>N:9WX|p_^n3Wp)^dvUzÿ,YjC*eYk RGۅƉtCW4bEȕibH$V(Ҫ]k&Ѱ Euz)wx]!f7#n&r>&$xqspxr<rOjOv_CZ/3 +u 9 ߶PڮrdL]s1(O8c2<vjQ㚉 -j,=]u~d.IAY,'Ly2Gs7ikVcN /!x@2Po}A([Cw6 :O_K_kAd!S/}*[gKbb褨ۨpjA|A%Ы("xx?O1ї0bJYtܥTMv{RߑPٺ-.U(U%oV?p+\QZ7wa\wQx‹ϥEo O#DM x xʹ)a.3 tfX'q_r.d9L]f:ue݋;WY[ǜ_Z ӫ'Fd#c 2,O|]ce *݉/Ҥ,{9꛴昤h91 G'Ê9ctgǘ9c/o A/Y1)24aYU=~|N۝aQMkYF\ҫBI$<D Itkk2. x!i7aK I mgC|Q]ƀ 0Ų cp1xR (5cEsPUSM"1Ea~?ΰxԀ6sd_ l޺_l4[)l?Ρ9iUVv;a=yJB枥zP[B\hN~lfVʟ G?"l,yi1&>p&|z3,FQst3c.( ?]u3L]f058N^$j[tߊ{Y^Bsamf_f~ dP1.0Ze8I12h\.`qfrgMWkj+&u_Dpxp*dqKM:Y¢oFb:~ KF m`0SكIAX85chHs-Uέ%QD+޿]88 $6H:  TB۬3,oMg0VZSx"^#lM&d`m^Dq΋JL~ɵ̽.g2 Z/&J-8;x㤍0FVa_ԙ{TvaiKM3ay7)}m ?s7|N-nvQH.r䆷_eFQ=tkkSyx\&?%SA8֠\0a vswnM'y(VqwͬnӸHի>h1^mQ9w|Nhj:-Gd;tę&qDWB"Bb%zG0A tr4kl]D |탶=.ZARI ;6qh/b2E'bRCAs[~Wk=ӒLPDt3h N*jGWkhc+?&cVd-2JD= IY6~VN2+Lq1?-&nYɇ:L=^4n*UN1R"2$݂nܣ] jL-&&HC^2Do/n[zY2Xgجs*$`:^(-1|S IH:?k9:}:%dN{pK߶{zϫ/r4{셂nA8ONd1@q %OK9:$-l= Wq{Xw#g$K\9-.Kf|J{UJ5T n޾͹(2;vٍii$rܛdBk(L,e>@ Xvgn!£Nuܸsf@RBC5H_p!4ix{Ylň'A$zxtdsi}_iZS*B$')kSohR4}hܜc(Dby҈+Տ)HZw.Lx[ Lf3xeroVQ4,[;N V lX8p|p,.YWwT~c3heODf*=,WRN@fOMTy^Vb\7+8B#$~0"͒+*ȥM!-1a&1Kf/$2LoUiZ;. `Yd:-!J#r!<+hke.츜 q- ᆲF=t8P&RlF7OOX \i!kذ}xEĴ}eܶ)"w=>C?`RU&t'cJT1?Vk oDr=%wbtgsĞŕm]`pvƼR(yv_@ojz(I]MG/…,mȞM-=5ֽ{O&;.*fsbVNm\Mf|٬ɔenp^LX]vCLf7R: "Ԙl汑n |Octoο*3# S`QA9v8&ofQ.(aJg ^NvG'u~}ۤ6fi5t< q%4T>AM5 7To;_:(%0+*}㩪)\[z*>jrJHB&KO%Iݙuj˜E+4D:X@wh 7$ukEOd04ԍ'se㐘GS%~[#\Z:9V47HϠ s\9j MB}-2n0m76w4{r.O dBoy}p]+nCT$Gv.Y'o3{,׶X t՟?>()D t&z\Ujʁ{QT_[8I`;]MZxJO4 6%Q5N + ~5ʤ 1|6<_< ? M}FSDXY s ^mFFp̔`qA^CBXSu\xFPD>Y, LM>5PĺM-lWr l/W*gEyˊC_ %dDyvywY>+Ǧ'`IL =P4>nl&< {pZC?'Q3W@ٴ'ԦJgvns}[w&!4.fؓ=t@MbT牧nPطbshQ,&a8YNZa{Aמp~y7Y`s A<>Z@0gQw!$PMډ6!W䋂vDQ]hXJ\AR̻=9K$@ z{Ϥ#s)#f6)[W. Οk!q't ]&{z_c3$Bo JRI\s %( ^*~A:q*M8Gg0EnA aP%zn-WIp -nEV&_=m|qOl׾  t #>zfpԡ.)$CAUd?m7w ˨"f|8mxA TB@E:3TRO+uWt# in+No.HͫIҢ¹%ԪA8J& ?b~+"O-.J8Қ"si (# X_@%W…8}0rAw'wk9zj0W<8_P]kv$uuی_^[N-e r21i Pa+|{,#ڼC[88Ֆ--Pw 0O"u>(kq6L{ WgZwtN=(m\%4Oj\hε?=|4gsS t0ğ l5GBũviek ־.r5`ʾNf !d)S3)L%q:[9TKs`R4>Ƽqpwx ^x4xɰ P4 'ϓ}^t{#_QegP!bZeZpydܰx>)*&UU&"[Űb;Pwa2RL#%Zt2ZfJH̜ hbA+⣂($G#"%S )!L2vg?~}m:JǓ,bpq%^'DRåefVm8*NK?OKB ^JixJ 7Ar{#7)גBL$B5,)=43kÏtbXHٔKʱ_LvRƷLxNLnC '* q4o|//`*}|1PcӚ.Rlڊ3A8Lbзx?)0J$^8IIh,7&&d+rYr'~Z:{ct)T{.Cf7/K~0+3XkhT<>@9B髺]Aɒ)޹#w\AkQhxٜ.Po`b[x\v-ck-4mCZU YqFRhA|)gan`QӦgCq2gU_Lk,V/!G#@4{}P>ԽY7Fyi}/wUͅa[iLPHq{W0׃)>o8pc3+IZ\ S $xt}sHA 0gpV?=uW?nygǃjN0`;"Lm[ ;'NiN\WtcB4?G9È^ &I\HPKvgfuse-zip-0.2.13/tests/blackbox/data/not-full-path.zip0000644000000000000000000000047711477217250022371 0ustar rootroot00000000000000PK i= foo/barUT ILILux blah-blah PK U~=$bebebeUT !L!Lux bebe PK i= foo/barUTILux PK U~=$KbebebeUT!Lux PKfuse-zip-0.2.13/tests/blackbox/fuse-zip.test0000755000000000000000000011313511477217250020704 0ustar rootroot00000000000000#!/bin/sh # \ exec tclsh "$0" "$@" package require Tcl 8.5 package require BLT package require Tclx package require cmdline package require struct::set package require control ::control::control assert enabled true namespace eval ::fusezip::test { namespace import ::control::assert variable binary ../../fuse-zip variable tmpdir [ exec mktemp -d "/tmp/fuse-zip-tests-[ pid ].XXXXXXXXXX" ] variable timeout [ expr {1000*5} ] variable valgrind false variable listTests false variable status {} variable error {} variable output {} variable initializationState {} variable numPassed 0 variable numFailed 0 variable numSkipped 0 variable failed {} # parseArgs -- # # Parse commnd-line arguments. # Keys -list, -valgrind, -listfailed are recognized, all other parameters # added to match list to filter tests. proc parseArgs {} { variable matchList upvar #0 argv argv set options { {list "List tests and exit"} {valgrind "Run tests under valgrind and check memory errors"} {-help "Print this message"} } if {[ catch { array set p [ ::cmdline::getKnownOptions argv $options ] } msg ]} { puts stderr $msg exit 1 } if {$p(-help)} { puts [ ::cmdline::usage $options ] exit 1 } variable listTests $p(list) variable valgrind $p(valgrind) if {[ llength $argv ] != 0} { set matchList $argv } else { set matchList * } } # wrapIfNeeded -- # # Wrap executable using valgrind if valgrinding is enabled (environment # variable VALGRIND is set to 1). # If env. variable VALGRIND_ARGS is present, it recognized as # space-separated list of additional arguments for valgrind (for example, # VALGRIND_ARGS="--gen-suppressions=all"). proc wrapIfNeeded {command} { variable valgrind if {$valgrind} { set additional {} if {[ info exists ::env(VALGRIND_ARGS) ]} { set additional [ split $::env(VALGRIND_ARGS) ] } set res [ list valgrind -q --leak-check=full --track-origins=yes --error-exitcode=33 --suppressions=valgrind.supp {*}$additional ] } else { set res {} } lappend res $command return $res } # fstest -- # # Execute 'script', check return code and clean up. # If script exited with error, error information and filesystem stderr # printed. # Test is executed only if id matches at least one pattern in matchList. # # Arguments: # id Test ID # description Test Description # script Test body proc fstest {id description script} { variable mounted false variable error variable numPassed variable numFailed variable numSkipped variable tmpdir variable mountdir variable matchList variable failed upvar fname fname set fname {} set do false foreach match $matchList { if {[ string match $match $id ]} { set do true break } } if {!$do} { incr numSkipped return } file mkdir $tmpdir set mountdir "[ file join $tmpdir mountPoint ]-$id" file mkdir $mountdir set code [ catch { if {[ catch { uplevel $script } err opts ]} { if {[ catch { umount } err2 opts2 ]} { dict set opts2 -cause $opts return -options $opts2 } return -options $opts } } ret opts ] switch $code { 0 { # TCL_OK incr numPassed } 1 { # TCL_ERROR forceumount puts "\nTest `$id' ($description) failed." puts "" puts "Error: [ dict get $opts -errorinfo ]" puts "Error code [ dict get $opts -code ], errorcode [ dict get $opts -errorcode ]" if {[ dict exists $opts -cause ]} { set opts [ dict get $opts -cause ] puts "Caused by:" puts "Error: [ dict get $opts -errorinfo ]" puts "Error code [ dict get $opts -code ], errorcode [ dict get $opts -errorcode ]" } puts "" puts "Filesystem output: [ join $error \n ]" lappend failed $id incr numFailed } default { puts "\nTest `$id' ($description) returned incorrect code($code): [ lindex {OK ERROR RETURN BREAK CONTINUE} $code ]" incr numSkipped } } file delete -force $tmpdir } # mount -- # # Mount fuse-zip on archive fname to mountdir. # Filesystem process started in background but without detaching from # terminal and with debug mode enabled. Messages sent to stderr are # parsed to determine finish of file system initialization process. # # Variables: # fname Archive file name (from caller context) # Arguments: # args Additional file system arguments proc mount {args} { upvar fname fname variable binary variable mountdir variable stopped variable output variable error variable mounted variable initializationState set ns [ namespace current ] set ${ns}::status {STARTED} # if file system stopped in abnormal way, variable # 'initializationState' is set to exit from vwait block. set cmd [ list ${ns}::status {write unset} \ [ list apply [ list {args} { variable initializationState exited } $ns ] ] \ ] trace add variable {*}$cmd set output {} set error {} blt::bgexec ${ns}::status \ -output ${ns}::output \ -onerror ${ns}::processError \ -linebuffered true \ {*}[ wrapIfNeeded $binary ] {*}$args -d $fname $mountdir & vwait ${ns}::initializationState trace remove variable {*}$cmd if {$initializationState eq "exited"} { error "Filesystem not mounted (status=[ set ${ns}::status ])" {} NOTMOUNTED } set mounted true } # processError -- # # Process each line of stderr of called file system. # Each line is appended to 'error' list to accumulate full output. # If first message successfully went from filesystem, it assumed as # mounted and listeners are notified by setting variable # 'initializationState'. # # Arguments: # data Line to process proc processError {data} { variable initializationState variable error if {[ regexp {^ unique: 1,.*[Ss]uccess.*, outsize: \d+$} $data ]} { set initializationState success } lappend error $data } # umount -- # # Unmount filesystem. # Firstly try to umount FS in a standard way via fusermount -uz. # If file system is not unmounted in a specified timeout, kill filesystem # process and unmount in the hard way (fusermount -uz). # If file system is already unmounted. do nothing. proc umount {} { variable mounted variable mountdir variable status variable timeout variable output variable error variable valgrind if {!$mounted} { return } set ns [ namespace current ] set statusVar ${ns}::status if {$status ne ""} { # not yet stopped if {[ catch { exec fusermount -uz $mountdir } err opts ]} { puts "Fusermount error: $err" } set afterId [ after $timeout [ list set $statusVar KILLED ] ] vwait $statusVar after cancel $afterId } lassign $status state pid code msg set status {} set mounted false if {$state eq "KILLED"} { catch {exec fusermount -uz $mountdir} error "Filesystem unmounting timed out" } else { if {$code != 0} { error "Filesystem returned error (code=$code)" } if {$output ne ""} { error "Unexpected output from filesystem: $output" } } } # forceumount -- # # Force kill filesystem process and free mountpoint proc forceumount {} { variable mountdir variable status catch {exec fusermount -u $mountdir} set [ namespace current ]::status FORCE-KILLED catch {exec fusermount -uz $mountdir} } # create -- # # Create specified list of files and directories in specified directory # (relative to tmpdir). # # Arguments: # dir Destination directory # files List of files and directories to add # Format of item: # / proc createContent {dir files} { variable tmpdir file delete -force $tmpdir/$dir file mkdir $tmpdir/$dir set pos 0 while {$pos < [ llength $files ]} { set name [ lindex $files $pos ] incr pos if {[ string index $name end ] == "/"} { file mkdir $tmpdir/$dir/$name } else { makeFile [ lindex $files $pos ] $dir/$name incr pos } } } # create -- # # Create archive for tests containing specified list of files and # directories and set it name to variable fname in caller context. # # Arguments: # files List of files (as for createContent command) proc create {files} { variable tmpdir upvar fname fname set fname $tmpdir/test.zip createContent archiveSource $files set pwd [ pwd ] cd $tmpdir/archiveSource if {[ catch { exec zip -r $fname {*}[ glob * ] } err opts ]} { cd $pwd return -options $opts } cd $pwd } # check -- # # Check archive integrity by using 'unzip -t' command. # Check file content with expected. # If any problem detected, error is thrown. # # Arguments: # files List of files (as for createContent command) proc check {files} { upvar fname fname variable tmpdir exec unzip -t $fname createContent expectedResult $files file delete -force $tmpdir/testResult file mkdir $tmpdir/testResult exec unzip -nd $tmpdir/testResult $fname exec diff -ura $tmpdir/testResult $tmpdir/expectedResult } # stripValgrindOutput -- # # Strip valgrind's output from data. If valgrinding is not enabled, # return data as is. proc stripValgrindOutput {data} { set res {} foreach line [ split $data "\n" ] { if {![ regexp {^==\d+== } $line ]} { lappend res $line } } return [ join $res "\n" ] } # makeFile -- # # Version of tclTest::makeFile that does not creates newline at the end # of file. proc makeFile {contents name {directory {}}} { variable tmpdir if {$directory eq ""} { set fname $tmpdir/$name } else { set fname $directory/$name } file mkdir [ file dirname $fname ] set f [ open $fname w ] puts -nonewline $f $contents close $f } # finalize -- # # Clean up temporary files, print statistics and exit. proc finalize {} { variable numPassed variable numFailed variable numSkipped variable tmpdir variable listTests variable failed forceumount file delete -force $tmpdir if {!$listTests} { if {[ llength $failed ] > 0} { puts "Failed tests: [ join $failed ]" } puts [ format {Total: %3d Passed: %3d Failed: %3d Skipped %3d} \ [ expr {$numPassed + $numFailed + $numSkipped} ] \ $numPassed $numFailed $numSkipped \ ] if {$numFailed != 0} { exit 1 } else { exit 0 } } else { exit 0 } } ############################################################ # INITIALIZATION ############################################################ parseArgs signal trap {HUP INT TERM} "puts stderr Interrupt; ::[ namespace current ]::finalize" if {$listTests} { # Stub for fstest that only prints test IDs and descriptions proc fstest {id description script} { puts [ format "%-40s %s" $id $description ] } } ############################################################ # TESTS ############################################################ fstest usage {Usage test} { lassign [ pipe ] r w if {![ catch {exec -ignorestderr {*}[ wrapIfNeeded $binary ] 2>@ $w} res opts ]} { close $w close $r error "Error code should be non-zero!" } close $w set data [ string trim [ read $r ] ] close $r if {![ regexp {^usage: fuse-zip } [ stripValgrindOutput $data ] ]} { error "Usage info expected, but `$data' given" } } fstest version {Version test} { lassign [ pipe ] r w if {![ catch {exec -ignorestderr {*}[ wrapIfNeeded $binary ] -V 2>@ $w} res opts ]} { close $w close $r error "Error code should be non-zero!" } close $w set data [ string trim [ read $r ] ] close $r if {![ regexp {^(.*?version.*?\d+\.\d+\n)+$} \ "[ stripValgrindOutput $data ]\n" ]} { error "Version info expected" } } fstest bad-mountpoint {Bad mountpoint} { file delete -force $mountdir if {![ catch {mount} err opts ] && [ dict get $opts -errorcode ] eq "NOTMOUNTED"} { error "Mount error is expected" } else { set msg [ stripValgrindOutput [ join $error \n ] ] if {![ regexp {^fuse: bad mount point} $msg ]} { error "Invalid error message" } } } fstest bad-archive-crc {Bad archive} { set fname data/bad-archive.zip mount if {![catch {open $mountdir/bash.txt r} err opts]} { error "File read error is expected" } else { set code [dict get $opts -errorcode] assert {[lrange $code 0 1] == {POSIX EIO}} } umount } fstest mount-umount {Mount and unmount nonexistent file} { set fname $tmpdir/nonexistent.zip mount umount if {[ file exist $fname ]} { error "Archive should not exist!" } } fstest add-file-to-empty-archive {Add file to empty archive} { set fname $tmpdir/empty.zip mount makeFile {file content} somefile $mountdir umount check { somefile {file content} } } fstest add-file {Add file to archive} { create { foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar } mount makeFile moo-moo moo $mountdir umount check { moo moo-moo foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar } } fstest add-dir {Add directory to archive} { create { foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar } mount file mkdir $mountdir/foo/first file mkdir $mountdir/foo/first/second umount check { foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar foo/first/ foo/first/second/ } } fstest add-remove-file {Add file to archive and remove it} { create { foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar } mount makeFile moo-moo moo $mountdir file delete $mountdir/moo umount check { foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar } } fstest add-truncate-file {Add file to archive and truncate it} { create { foo.bar foobar } mount makeFile moo-moo moo $mountdir ftruncate $mountdir/moo 3 umount check { foo.bar foobar moo moo } } fstest add-truncate-remove-file {Add file to archive, truncate and remove it} { create { foo.bar foobar } mount makeFile moo-moo moo $mountdir ftruncate $mountdir/moo 3 file delete $mountdir/moo umount check { foo.bar foobar } } fstest add-overwrite-file {Add file to archive and overwrite it} { create { foo.bar foobar } mount makeFile moo-moo moo $mountdir makeFile Gerasim moo $mountdir umount check { foo.bar foobar moo Gerasim } } fstest remove-file {Remove file from archive} { create { filename.ext blah-blah filename2.ext blah-blah } mount file delete $mountdir/filename.ext umount check { filename2.ext blah-blah } } fstest remove-dir {Remove directory from archive} { create { filename.ext blah-blah foo/ foo/moo {} bar/ } mount file delete -force $mountdir/foo umount check { filename.ext blah-blah bar/ } } fstest remove-nonexistent-dir {Remove nonexistent directory from archive} { create { filename.ext blah-blah bar/ } mount if {![ catch { exec rmdir $mountdir/foo } err opts ]} { error "rmdir error is expected!" } umount check { filename.ext blah-blah bar/ } } fstest remove-last-file {Remove last file from archive} { create { filename.ext blah-blah } mount file delete $mountdir/filename.ext umount if {[ file exist $fname ]} { puts [ exec cat $fname ] error "Archive should not exist!" } } fstest overwrite-file {Overwrite existing file} { create { filename.ext blah-blah } mount set f [ open $mountdir/filename.ext w ] puts $f "Be-be-be!" close $f umount check { filename.ext "Be-be-be!\n" } } fstest truncate {Truncate existing file} { create { filename.ext blah-blah } mount ftruncate $mountdir/filename.ext 5 set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data "blah-" ] != 0} { error "Invalid content: $data" } umount check { filename.ext blah- } } fstest truncate-remove {Truncate existing file and delete it} { create { filename.ext blah-blah other other } mount ftruncate $mountdir/filename.ext 5 file delete $mountdir/filename.ext umount check { other other } } fstest truncate-on-chunk-boundary {Truncate existing file on a chunk boundary} { create [ list \ filename.ext [ string repeat a 8192 ] ] mount ftruncate $mountdir/filename.ext 4096 umount check [ list \ filename.ext [ string repeat a 4096 ] \ ] } fstest truncate-to-zero {Truncate existing file (to zero size)} { create { filename.ext blah-blah } mount ftruncate $mountdir/filename.ext 0 umount check { filename.ext {} } } fstest truncate-after-end-same-chunk {Truncate existing file after the end (without new chunks creation)} { create { filename.ext blah-blah } mount ftruncate $mountdir/filename.ext 11 set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data "blah-blah\x00\x00" ] != 0} { error "Invalid content: $data" } umount check { filename.ext "blah-blah\x00\x00" } } fstest truncate-after-end-other-chunk {Truncate existing file after the end (with new chunk creation)} { create { filename.ext blah-blah } mount ftruncate $mountdir/filename.ext 5009 set content "blah-blah[ string repeat "\x00" 5000 ]" set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data $content ] != 0} { error "Invalid content: $data" } umount check [ list \ filename.ext $content \ ] } fstest truncate-expand-read {Truncate file, expand it and read} { create { filename.ext blah-blah } mount ftruncate $mountdir/filename.ext 4 ftruncate $mountdir/filename.ext 9 set content "blah[ string repeat "\x00" 5 ]" set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data $content ] != 0} { error "Invalid content: $data" } umount check [ list \ filename.ext $content \ ] } fstest truncate-after-end-other-chunk-twice {Truncate existing file after the end (with new chunk creation, twice)} { create { filename.ext blah-blah } mount ftruncate $mountdir/filename.ext 5 ftruncate $mountdir/filename.ext 5005 set content "blah-[ string repeat "\x00" 5000 ]" set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data $content ] != 0} { error "Invalid content: $data" } umount check [ list \ filename.ext $content \ ] } fstest sparse-file {Sparse files test} { set fname $tmpdir/archive.zip mount set count 8201 set f [ open $mountdir/filename.ext w ] seek $f $count start puts -nonewline $f blah close $f set content "[ string repeat "\x00" $count ]blah" set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $content $data ] != 0} { error "Invalid file content: $data" } umount check [ list \ filename.ext $content \ ] } fstest sparse-file-2 {Sparse files test 2} { create { filename.ext start } mount set count 4096 set f [ open $mountdir/filename.ext r+ ] seek $f [ expr {5+$count} ] start puts -nonewline $f end close $f set content "start[ string repeat "\x00" $count ]end" set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $content $data ] != 0} { error "Invalid file content: $data" } umount check [ list \ filename.ext $content \ ] } fstest sparse-truncate {Truncate sparse file} { create { filename.ext {} } mount set count 4078 set f [ open $mountdir/filename.ext r+ ] seek $f 8208 start puts -nonewline $f data flush $f ftruncate -fileid $f $count close $f set content "[ string repeat "\x00" $count ]" set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $content $data ] != 0} { error "Invalid file content: $data" } umount check [ list \ filename.ext $content \ ] } fstest read-zero {Read zero-length file from archive} { create { filename.ext {} } mount set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data "" ] != 0} { error "Invalid content: $data" } umount } fstest read-file {Read file from archive} { create { filename.ext blah-blah } mount set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data "blah-blah" ] != 0} { error "Invalid content: $data" } umount } fstest read-file-two-handles {Read file from archive (two handles)} { set content [ string repeat "abcdef" 3000 ] set l [ string length $content ] create [ list \ filename.ext $content \ ] mount set f1 [ open $mountdir/filename.ext r ] set f2 [ open $mountdir/filename.ext r ] set positions { {0 6} {10 1000} {4097 322} {27 15} {1 1} {3333 11} } for {set i 0} {$i < [ llength $positions ]} {incr i} { lassign [ lindex $positions $i ] off size seek $f1 $off set data [ read $f1 $size ] set expected [ string range $content $off [ expr {$off + $size - 1} ] ] assert {$data eq $expected} lassign [ lindex $positions end-$i ] off size seek $f2 $off set data [ read $f2 $size ] set expected [ string range $content $off [ expr {$off + $size - 1} ] ] assert {$data eq $expected} } close $f1 close $f2 umount } fstest read-file-random-access {Read file from archive (random access)} { set content [ string repeat "abcdef" 3000 ] set l [ string length $content ] create [ list \ filename.ext $content \ ] mount set f [ open $mountdir/filename.ext r ] foreach {off size} { 0 6 10 1000 4097 322 27 15 1 1 3333 11 } { seek $f $off set data [ read $f $size ] set expected [ string range $content $off [ expr {$off + $size - 1} ] ] assert {$data eq $expected} } close $f umount } fstest read-file-three-chunks {Read file from archive (3 chunks)} { set content [ string repeat "a" [ expr {4096*3-100} ] ] create [ list \ filename.ext $content \ ] mount set f [ open $mountdir/filename.ext r ] set data [ read $f ] close $f if {[ string compare $data $content ] != 0} { error "Invalid content: '$data'" } umount } fstest read-on-end {Read on the end of file} { set content [ string repeat "a" 100 ] create [ list \ filename.ext $content \ ] mount set f [ open $mountdir/filename.ext r ] fconfigure $f -encoding binary seek $f 98 set data [ read $f 4 ] close $f if {[ string compare $data "aa" ] != 0} { error "Invalid content: '$data'" } umount } fstest read-after-end {Read after the end of file} { set content [ string repeat "b" 100 ] create [ list \ filename.ext $content \ ] mount set f [ open $mountdir/filename.ext r ] fconfigure $f -encoding binary seek $f 102 set data [ read $f 4 ] close $f if {[ string compare $data "" ] != 0} { error "Invalid content: '$data'" } umount } fstest read-bad-crc {Read file from archive (bad CRC)} { set fname data/bad-crc.zip mount set res [ catch { set f [ open $mountdir/bash.txt r ] set data [ read $f ] close $f } err opts ] umount if {$res == 0 || [ lrange [ dict get $opts -errorcode ] 0 1 ] ne "POSIX EIO"} { error "Read error expected" } } fstest read-bad-crc-twice {Read file from archive (bad CRC, twice), changeset 80ad59679639} { set fname data/bad-crc.zip mount foreach step {1st 2nd} { set res [ catch { set f [ open $mountdir/bash.txt r ] set data [ read $f ] close $f } err opts ] if {$res == 0 || [ lrange [ dict get $opts -errorcode ] 0 1 ] ne "POSIX EIO"} { error "Read error expected on step $step" } } umount } fstest find {Check that find is working on a filesystem} { create { foo.bar foobar f/ f/o/ f/o/o content foo/ foo/bar foo-bar } set in { foo.bar f f/o f/o/o foo foo/bar } mount set data {} set l [ string length "$mountdir/" ] foreach line [ split [ exec find $mountdir ] "\n" ] { if {[ string length $line ] > $l} { lappend data [ string range $line $l end ] } } umount if {[ lsort $in ] != [ lsort $data ]} { error "Invalid list of files returned: $data" } } fstest append {Append to file} { create { foo foo } mount set f [ open $mountdir/foo a ] puts -nonewline $f bar close $f umount check { foo foobar } } fstest compare-source-and-mountpoint {Compare source files with mountpoint content} { create [ list \ foo.bar foobar \ f/ \ f/o/ \ f/o/o content \ foo/ \ foo/bar foo-bar \ bigFile [ string repeat "substances! " 9000 ] \ zerofile {} \ longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName_longName {} ] mount exec diff -Nura $tmpdir/archiveSource $mountdir umount } fstest iconv-cp866 {Check that archive from 'other' OS correctly mounted} { set fname data/cp866.zip mount -o modules=iconv,from_code=cp866 if {[ lsort [ glob -directory $mountdir -tails * ] ] ne [ lsort "{\u0422\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0439 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442.txt} \u0414\u0430\u0442\u0430" ]} { error "File names incorrectly decoded: [ glob -directory $mountdir * ]" } umount } fstest rename-file {Rename file} { create { foo.bar foobar } mount file rename $mountdir/foo.bar $mountdir/john_doe umount check { john_doe foobar } } fstest rename-file-new-dir {Rename file (new directory creation)} { create { foo.bar foobar squirrel belka } mount file mkdir $mountdir/test file rename $mountdir/foo.bar $mountdir/test/john_doe set ls [ glob -directory $mountdir -tails * ] if {![ ::struct::set equal {test squirrel} $ls ]} { error "Invalid files in /: $ls" } set ls [ glob -directory $mountdir/test -tails * ] if {![ ::struct::set equal {john_doe} $ls ]} { error "Invalid files in /test: $ls" } umount check { test/john_doe foobar squirrel belka } } fstest rename-file-new-path {Rename file (change directory)} { create { foo.bar foobar test/ test/blah {blah-blah} } mount file rename $mountdir/foo.bar $mountdir/test/john_doe set ls [ glob -directory $mountdir -tails * ] if {![ ::struct::set equal {test} $ls ]} { error "Invalid files in /: $ls" } set ls [ glob -directory $mountdir/test -tails * ] if {![ ::struct::set equal {blah john_doe} $ls ]} { error "Invalid files in /test: $ls" } umount check { test/john_doe foobar test/blah {blah-blah} } } fstest rename-empty-dir {Rename empty directory} { create { foo/ } mount file rename $mountdir/foo $mountdir/bar umount check { bar/ } } fstest rename-non-empty-dir {Rename non-empty directory} { create { foo/john/doe {Hi} foo/bar {content} foo/duck/tape/1 1 } mount file rename $mountdir/foo $mountdir/bar umount check { bar/john/doe {Hi} bar/bar {content} bar/duck/tape/1 1 } } fstest rename-non-empty-dir-new-path {Rename non-empty directory (new dir)} { create { foo/bar {content} foo/john/doe {Hi} foo/duck/tape/ } mount file mkdir $mountdir/bar file rename $mountdir/foo $mountdir/bar/duck umount check { bar/duck/bar {content} bar/duck/john/doe {Hi} bar/duck/duck/tape/ } } fstest rename-file-to-existent {Renamed file to existent file} { create { kitten {Meow} puppy {Woof} } mount file rename -force $mountdir/kitten $mountdir/puppy umount check { puppy {Meow} } } fstest size-file {Check file size} { create { foobar blah-blah } mount assert {[ file size $mountdir/foobar ] == 9} makeFile doe john $mountdir assert {[ file size $mountdir/john ] == 3} umount check { foobar blah-blah john doe } } fstest size-dir {Check directory size} { create { foobar/ } mount assert {[ file size $mountdir/foobar ] == 0} file mkdir $mountdir/john assert {[ file size $mountdir/john ] == 0} umount check { foobar/ john/ } } fstest read-only-mode {Check read only mode} { create { foo.bar foobar } file copy $fname $fname-copy mount -r umount exec diff $fname $fname-copy } fstest dir-stub-read-only {Check that archive is not modified after mount/unmount in case of stub directories is created while loading file tree} { file copy data/not-full-path.zip $tmpdir/not-full-path.zip set fname $tmpdir/not-full-path.zip mount -r assert {[file exists $mountdir/foo/bar]} umount exec diff $fname data/not-full-path.zip } fstest dir-stub-rename {Check stub dirs rename} { file copy data/not-full-path.zip $tmpdir/not-full-path.zip set fname $tmpdir/not-full-path.zip mount assert {[file exists $mountdir/foo/bar]} file rename $mountdir/foo $mountdir/foofoo umount check { bebebe bebe\n foofoo/ foofoo/bar blah-blah\n } } fstest dir-stub-rmdir {Check that stubbed dir removed successfully} { file copy data/not-full-path.zip $tmpdir/not-full-path.zip set fname $tmpdir/not-full-path.zip mount file delete -force $mountdir/foo umount check { bebebe bebe\n } } fstest mkdir-rmdir {Check that mkdir()-rmdir() success} { create { a b } mount file mkdir $mountdir/foo file delete $mountdir/foo umount check { a b } } fstest rmdir {Check that rmdir() success on non-stub directory} { create { a b foo/ } mount file delete $mountdir/foo umount check { a b } } finalize } namespace delete ::util::test # vim: set ft=tcl: fuse-zip-0.2.13/tests/blackbox/valgrind.supp0000644000000000000000000000220411477217250020747 0ustar rootroot00000000000000# add-dir, truncate-to-zero { Unitialized memory read in zlib's deflate (not a problem) Memcheck:Cond fun:deflate obj:/usr/lib/libzip.so.1.0.0 fun:zip_close fun:_ZN11FuseZipDataD1Ev fun:fusezip_destroy fun:fuse_fs_destroy obj:/usr/lib/libfuse.so.2.8.1 obj:/usr/lib/libfuse.so.2.8.1 fun:fuse_session_destroy fun:fuse_destroy obj:/usr/lib/libfuse.so.2.8.1 fun:main } # truncate-on-chunk-boundary { Unitialized memory read in zlib's inflateReset2 (not a problem) Memcheck:Cond fun:inflateReset2 fun:inflateInit2_ fun:zip_fopen_index fun:_ZN9BigBufferC1EP3zipil fun:_ZN8FileNode4openEv fun:fusezip_truncate obj:/usr/lib/libfuse.so.2.8.1 obj:/usr/lib/libfuse.so.2.8.1 fun:fuse_session_loop fun:main } # read-file-three-chunks { Unitialized memory read in zlib's inflateReset2 (not a problem) Memcheck:Cond fun:inflateReset2 fun:inflateInit2_ fun:zip_fopen_index fun:_ZN9BigBufferC1EP3zipil fun:_ZN8FileNode4openEv fun:fusezip_open fun:fuse_fs_open obj:/usr/lib/libfuse.so.2.8.1 obj:/usr/lib/libfuse.so.2.8.1 fun:fuse_session_loop fun:main } fuse-zip-0.2.13/tests/whitebox/Makefile0000644000000000000000000000166311477217250017744 0ustar rootroot00000000000000CXXFLAGS=-g -O2 -Wall -Wextra FUSEFLAGS=$(shell pkg-config fuse --cflags) VALGRIND=valgrind -q --leak-check=full --track-origins=yes --error-exitcode=33 LIB=../../lib/libfusezip.a SOURCES=$(wildcard *.cpp) OBJECTS=$(SOURCES:.cpp=.o) DEST=$(OBJECTS:.o=.x) TESTS=$(DEST:.x=.test) VALGRIND_TESTS=$(DEST:.x=.valgrind) all: $(DEST) $(DEST): %.x: %.o $(LIB) $(CXX) $(LDFLAGS) $< \ -L../../lib -lfusezip \ -o $@ $(OBJECTS): %.o: %.cpp $(CXX) -c $(CXXFLAGS) $(FUSEFLAGS) $(ZIPFLAGS) \ -I../../lib \ $< -o $@ $(LIB): make -C ../../lib lib-clean: make -C ../../lib clean clean: lib-clean test-clean rm -f *.o $(DEST) $(OBJECTS) distclean: clean make -C ../../lib clean list: @echo $(TESTS) test: $(TESTS) test-clean: rm -f *.x.core vgcore.* $(TESTS): %.test: %.x ./$< $(VALGRIND_TESTS): %.valgrind: %.x $(VALGRIND) ./$< valgrind: $(VALGRIND_TESTS) .PHONY: all clean distclean test test-clean valgrind lib-clean fuse-zip-0.2.13/tests/whitebox/bigBufferTest.cpp0000644000000000000000000003054711477217250021546 0ustar rootroot00000000000000#include "../config.h" #include #include #include #include #include // Public Morozoff design pattern :) #define private public #include "bigBuffer.h" #include "common.h" // global variables // is libzip functions should be called? bool use_zip = false; // libzip stub structures struct zip { bool fail_zip_fopen_index; bool fail_zip_fread; bool fail_zip_fclose; bool fail_zip_source_function; bool fail_zip_add; bool fail_zip_replace; struct zip_source *source; }; struct zip_file { struct zip *zip; }; struct zip_source { struct zip *zip; void *cbs; }; // libzip stub functions int zip_add(struct zip *z, const char *, struct zip_source *) { assert(use_zip); return z->fail_zip_add ? -1 : 0; } int zip_replace(struct zip *z, int, struct zip_source *) { assert(use_zip); return z->fail_zip_replace ? -1 : 0; } struct zip_file *zip_fopen_index(struct zip *z, int, int) { assert(use_zip); if (z->fail_zip_fopen_index) { return NULL; } else { struct zip_file *res = (struct zip_file *)malloc(sizeof(struct zip_file)); res->zip = z; return res; } } ssize_t zip_fread(struct zip_file *zf, void *dest, size_t size) { assert(use_zip); if (zf->zip->fail_zip_fread) { return -1; } else { memset(dest, 'X', size); return size; } } int zip_fclose(struct zip_file *zf) { assert(use_zip); bool fail = zf->zip->fail_zip_fclose; free(zf); return fail ? -1 : 0; } struct zip_source *zip_source_function(struct zip *z, zip_source_callback, void *cbs) { assert(use_zip); if (z->fail_zip_source_function) { return NULL; } else { struct zip_source *zs = (struct zip_source *)malloc(sizeof(struct zip_source)); zs->zip = z; zs->cbs = cbs; z->source = zs; return zs; } } void zip_source_free(struct zip_source *z) { assert(use_zip); assert(z->zip->fail_zip_add || z->zip->fail_zip_replace); free(z); } // only stubs struct zip *zip_open(const char *, int, int *) { assert(false); return NULL; } int zip_error_to_str(char *, size_t, int, int) { assert(false); return 0; } int zip_add_dir(struct zip *, const char *) { assert(false); return 0; } int zip_close(struct zip *) { assert(false); return 0; } int zip_delete(struct zip *, int) { assert(false); return 0; } int zip_get_num_files(struct zip *) { assert(false); return 0; } int zip_rename(struct zip *, int, const char *) { assert(false); return 0; } int zip_stat_index(struct zip *, int, int, struct zip_stat *) { assert(false); return 0; } const char *zip_strerror(struct zip *) { assert(false); return NULL; } //////////////////////////////////////////////////////////////////////////// // TESTS //////////////////////////////////////////////////////////////////////////// void chunkLocators() { // static functions test assert(BigBuffer::chunksCount(0) == 0); assert(BigBuffer::chunksCount(1) == 1); assert(BigBuffer::chunksCount(BigBuffer::chunkSize) == 1); assert(BigBuffer::chunksCount(BigBuffer::chunkSize - 1) == 1); assert(BigBuffer::chunksCount(BigBuffer::chunkSize + 1) == 2); assert(BigBuffer::chunksCount(BigBuffer::chunkSize * 2 - 1) == 2); assert(BigBuffer::chunkNumber(0) == 0); assert(BigBuffer::chunkNumber(1) == 0); assert(BigBuffer::chunkNumber(BigBuffer::chunkSize) == 1); assert(BigBuffer::chunkNumber(BigBuffer::chunkSize - 1) == 0); assert(BigBuffer::chunkNumber(BigBuffer::chunkSize + 1) == 1); assert(BigBuffer::chunkNumber(BigBuffer::chunkSize * 2 - 1) == 1); assert(BigBuffer::chunkOffset(0) == 0); assert(BigBuffer::chunkOffset(1) == 1); assert(BigBuffer::chunkOffset(BigBuffer::chunkSize) == 0); assert(BigBuffer::chunkOffset(BigBuffer::chunkSize - 1) == BigBuffer::chunkSize - 1); assert(BigBuffer::chunkOffset(BigBuffer::chunkSize + 1) == 1); assert(BigBuffer::chunkOffset(BigBuffer::chunkSize * 2 - 1) == BigBuffer::chunkSize - 1); } void createDelete() { BigBuffer bb; assert(bb.len == 0); } void truncate() { BigBuffer bb; bb.truncate(22); assert(bb.len == 22); bb.truncate(2); assert(bb.len == 2); bb.truncate(BigBuffer::chunkSize); assert(bb.len == BigBuffer::chunkSize); bb.truncate(BigBuffer::chunkSize + 1); assert(bb.len == BigBuffer::chunkSize + 1); bb.truncate(0); assert(bb.len == 0); } void readFile() { char buf[0xff]; char empty[0xff]; memset(empty, 0, 0xff); int nr; BigBuffer bb; nr = bb.read(buf, 100, 0); assert(nr == 0); nr = bb.read(buf, 100, 100); assert(nr == 0); bb.truncate(10); nr = bb.read(buf, 10, 0); assert(nr == 10); assert(memcmp(buf, empty, nr) == 0); bb.truncate(BigBuffer::chunkSize); nr = bb.read(buf, 10, BigBuffer::chunkSize - 5); assert(nr == 5); assert(memcmp(buf, empty, nr) == 0); } // read (size > chunkSize) void readFileOverChunkSize() { int n = BigBuffer::chunkSize * 3 + 15; char buf[n]; char empty[n]; memset(empty, 0, n); int nr; BigBuffer bb; nr = bb.read(buf, n, 0); assert(nr == 0); nr = bb.read(buf, n, 100); assert(nr == 0); bb.truncate(10); nr = bb.read(buf, 10, 0); assert(nr == 10); assert(memcmp(buf, empty, nr) == 0); bb.truncate(BigBuffer::chunkSize); nr = bb.read(buf, n, BigBuffer::chunkSize - 5); assert(nr == 5); assert(memcmp(buf, empty, nr) == 0); bb.truncate(BigBuffer::chunkSize * 2 - 12); nr = bb.read(buf, n, 1); assert(nr == BigBuffer::chunkSize * 2 - 12 - 1); assert(memcmp(buf, empty, nr) == 0); bb.truncate(BigBuffer::chunkSize * 10); nr = bb.read(buf, n, 1); assert(nr == n); assert(memcmp(buf, empty, nr) == 0); } // read data created by truncate void truncateRead() { char buf[BigBuffer::chunkSize]; char empty[BigBuffer::chunkSize]; memset(empty, 0, BigBuffer::chunkSize); BigBuffer b; b.truncate(BigBuffer::chunkSize); assert(b.len == BigBuffer::chunkSize); int nr = b.read(buf, BigBuffer::chunkSize, 0); assert((unsigned)nr == BigBuffer::chunkSize); assert(memcmp(buf, empty, BigBuffer::chunkSize) == 0); } // writing to file void writeFile() { char buf[0xff]; char buf2[0xff]; int nr, nw; BigBuffer bb; nw = bb.write(buf, 0, 0); assert(nw == 0); assert(bb.len == 0); memset(buf, 1, 10); memset(buf+10, 2, 10); nw = bb.write(buf, 20, 0); assert(nw == 20); assert(bb.len == 20); nr = bb.read(buf2, 30, 0); assert(nr == 20); assert(memcmp(buf, buf2, 20) == 0); bb.truncate(0); nw = bb.write(buf, 20, 0); assert(nw == 20); assert(bb.len == 20); nr = bb.read(buf2, 20, 10); assert(nr == 10); assert(memcmp(buf + 10, buf2, 10) == 0); } // read data from file expanded by write void readExpanded() { int n = BigBuffer::chunkSize * 2; char buf[n]; char expected[n]; memset(expected, 0, n); BigBuffer b; memset(buf, 'a', 10); memset(expected, 'a', 10); b.write(buf, 10, 0); assert(b.len == 10); memset(buf, 'z', 10); memset(expected + BigBuffer::chunkSize + 10, 'z', 10); b.write(buf, 10, BigBuffer::chunkSize + 10); assert(b.len == BigBuffer::chunkSize + 20); int nr = b.read(buf, n, 0); assert((unsigned)nr == BigBuffer::chunkSize + 20); assert(memcmp(buf, expected, nr) == 0); } // Test zip user function callback with empty file void zipUserFunctionCallBackEmpty() { BigBuffer bb; struct BigBuffer::CallBackStruct *cbs = new BigBuffer::CallBackStruct(); cbs->buf = &bb; cbs->mtime = 12345; struct zip_stat stat; assert(BigBuffer::zipUserFunctionCallback(cbs, &stat, 0, ZIP_SOURCE_STAT) == sizeof(struct zip_stat)); assert(stat.size == 0); assert(stat.mtime == 12345); assert(BigBuffer::zipUserFunctionCallback(cbs, NULL, 0, ZIP_SOURCE_OPEN) == 0); char buf[0xff]; assert(BigBuffer::zipUserFunctionCallback(cbs, buf, 0xff, ZIP_SOURCE_READ) == 0); assert(BigBuffer::zipUserFunctionCallback(cbs, NULL, 0, ZIP_SOURCE_CLOSE) == 0); assert(BigBuffer::zipUserFunctionCallback(cbs, NULL, 0, ZIP_SOURCE_FREE) == 0); } // Test zip user function callback with non-empty file void zipUserFunctionCallBackNonEmpty() { int n = BigBuffer::chunkSize*2; char buf[n]; memset(buf, 'f', n); BigBuffer bb; bb.write(buf, n, 0); struct BigBuffer::CallBackStruct *cbs = new BigBuffer::CallBackStruct(); cbs->buf = &bb; cbs->mtime = 0; struct zip_stat stat; assert(BigBuffer::zipUserFunctionCallback(cbs, &stat, 0, ZIP_SOURCE_STAT) == sizeof(struct zip_stat)); assert(stat.size == n); assert(stat.mtime == 0); assert(BigBuffer::zipUserFunctionCallback(cbs, NULL, 0, ZIP_SOURCE_OPEN) == 0); assert(BigBuffer::zipUserFunctionCallback(cbs, buf, BigBuffer::chunkSize, ZIP_SOURCE_READ) == BigBuffer::chunkSize); assert(BigBuffer::zipUserFunctionCallback(cbs, buf, BigBuffer::chunkSize, ZIP_SOURCE_READ) == BigBuffer::chunkSize); assert(BigBuffer::zipUserFunctionCallback(cbs, buf, BigBuffer::chunkSize, ZIP_SOURCE_READ) == 0); assert(BigBuffer::zipUserFunctionCallback(cbs, NULL, 0, ZIP_SOURCE_CLOSE) == 0); assert(BigBuffer::zipUserFunctionCallback(cbs, NULL, 0, ZIP_SOURCE_FREE) == 0); } // Read from zip file void readZip() { int size = 100; struct zip z; z.fail_zip_fopen_index = true; // invalid file { bool thrown = false; try { BigBuffer bb(&z, 1, size); } catch (const std::exception &e) { thrown = true; } assert(thrown); } z.fail_zip_fopen_index = false; z.fail_zip_fread = true; // read error { bool thrown = false; try { BigBuffer bb(&z, 2, size); } catch (const std::exception &e) { thrown = true; } assert(thrown); } z.fail_zip_fread = false; z.fail_zip_fclose = true; // close error { bool thrown = false; try { BigBuffer bb(&z, 3, size); } catch (const std::exception &e) { thrown = true; } assert(thrown); } z.fail_zip_fclose = false; // normal case { BigBuffer bb(&z, 0, size); char *buf = (char *)malloc(size); assert(bb.read(buf, size, 0) == size); for (int i = 0; i < size; ++i) { assert(buf[i] == 'X'); } free(buf); } } // Save file to zip // Check that saveToZip correctly working void writeZip() { // new file { BigBuffer bb; struct zip z; z.fail_zip_source_function = true; assert(bb.saveToZip(time(NULL), &z, "bebebe.txt", true, -1) == -ENOMEM); z.fail_zip_source_function = false; z.fail_zip_add = true; assert(bb.saveToZip(time(NULL), &z, "bebebe.txt", true, -1) == -ENOMEM); z.fail_zip_add = false; z.source = NULL; assert(bb.saveToZip(time(NULL), &z, "bebebe.txt", true, -1) == 0); delete (BigBuffer::CallBackStruct *)z.source->cbs; free(z.source); } // existing file { int size = 11111; struct zip z; z.fail_zip_fopen_index = false; z.fail_zip_fread = false; z.fail_zip_fclose = false; BigBuffer bb(&z, 0, size); z.fail_zip_source_function = true; assert(bb.saveToZip(time(NULL), &z, "bebebe.txt", false, 11) == -ENOMEM); z.fail_zip_source_function = false; z.fail_zip_replace = true; assert(bb.saveToZip(time(NULL), &z, "bebebe.txt", false, 11) == -ENOMEM); z.fail_zip_replace = false; z.source = NULL; assert(bb.saveToZip(time(NULL), &z, "bebebe.txt", false, 11) == 0); delete (BigBuffer::CallBackStruct *)z.source->cbs; free(z.source); } } int main(int, char **) { initTest(); chunkLocators(); createDelete(); truncate(); readFile(); readFileOverChunkSize(); truncateRead(); writeFile(); readExpanded(); zipUserFunctionCallBackEmpty(); zipUserFunctionCallBackNonEmpty(); use_zip = true; readZip(); writeZip(); return EXIT_SUCCESS; } fuse-zip-0.2.13/tests/whitebox/common.h0000644000000000000000000000453611477217250017747 0ustar rootroot00000000000000#include #include #include void zip_stat_init(struct zip_stat *sb) { memset(sb, 0, sizeof(struct zip_stat)); } void initTest() { // hide almost all messages setlogmask(LOG_MASK(LOG_EMERG)); } // Unused functions. Not called inside main program. void zip_error_clear(struct zip *) { assert(false); } void zip_error_get(struct zip *, int *, int *) { assert(false); } int zip_error_get_sys_type(int) { assert(false); return 0; } void zip_file_error_clear(struct zip_file *) { assert(false); } void zip_file_error_get(struct zip_file *, int *, int *) { assert(false); } const char *zip_file_strerror(struct zip_file *) { assert(false); return NULL; } struct zip_file *zip_fopen(struct zip *, const char *, int) { assert(false); return NULL; } const char *zip_get_archive_comment(struct zip *, int *, int) { assert(false); return NULL; } int zip_get_archive_flag(struct zip *, int, int) { assert(false); return 0; } const char *zip_get_file_comment(struct zip *, int, int *, int) { assert(false); return NULL; } const char *zip_get_name(struct zip *, int, int) { assert(false); return NULL; } int zip_name_locate(struct zip *, const char *, int) { assert(false); return 0; } int zip_set_archive_comment(struct zip *, const char *, int) { assert(false); return 0; } int zip_set_archive_flag(struct zip *, int, int) { assert(false); return 0; } int zip_set_file_comment(struct zip *, int, const char *, int) { assert(false); return 0; } struct zip_source *zip_source_buffer(struct zip *, const void *, off_t, int) { assert(false); return NULL; } struct zip_source *zip_source_file(struct zip *, const char *, off_t, off_t) { assert(false); return NULL; } struct zip_source *zip_source_filep(struct zip *, FILE *, off_t, off_t) { assert(false); return NULL; } struct zip_source *zip_source_zip(struct zip *, struct zip *, int, int, off_t, off_t) { assert(false); return NULL; } int zip_stat(struct zip *, const char *, int, struct zip_stat *) { assert(false); return 0; } int zip_unchange(struct zip *, int) { assert(false); return 0; } int zip_unchange_all(struct zip *) { assert(false); return 0; } int zip_unchange_archive(struct zip *) { assert(false); return 0; } fuse-zip-0.2.13/tests/whitebox/zip_open_failure.cpp0000644000000000000000000000366111477217250022342 0ustar rootroot00000000000000#include "../config.h" #include #include #include #include #include "fuse-zip.h" #include "fuseZipData.h" #include "common.h" // FUSE stub functions struct fuse_context *fuse_get_context(void) { return NULL; } // libzip stub structures struct zip {}; struct zip_file {}; struct zip_source {}; // libzip stub functions struct zip *zip_open(const char *, int, int *errorp) { *errorp = 0; return NULL; } int zip_error_to_str(char *buf, size_t len, int, int) { return strncpy(buf, "Expected error", len) - buf; } // only stubs int zip_add(struct zip *, const char *, struct zip_source *) { assert(false); return 0; } int zip_add_dir(struct zip *, const char *) { assert(false); return 0; } int zip_close(struct zip *) { assert(false); return 0; } int zip_delete(struct zip *, int) { assert(false); return 0; } int zip_fclose(struct zip_file *) { assert(false); return 0; } struct zip_file *zip_fopen_index(struct zip *, int, int) { assert(false); return NULL; } ssize_t zip_fread(struct zip_file *, void *, size_t) { assert(false); return 0; } int zip_get_num_files(struct zip *) { assert(false); return 0; } int zip_rename(struct zip *, int, const char *) { assert(false); return 0; } int zip_replace(struct zip *, int, struct zip_source *) { assert(false); return 0; } void zip_source_free(struct zip_source *) { assert(false); } struct zip_source *zip_source_function(struct zip *, zip_source_callback, void *) { assert(false); return NULL; } int zip_stat_index(struct zip *, int, int, struct zip_stat *) { assert(false); return 0; } const char *zip_strerror(struct zip *) { assert(false); return NULL; } // test functions int main(int, char **argv) { initTest(); FuseZipData *data = initFuseZip(argv[0], "test.zip"); assert(data == NULL); return EXIT_SUCCESS; }