skins2: For Linux, correct files that cannot be loaded because of a workaround that...
[vlc.git] / modules / gui / skins2 / src / theme_loader.cpp
1 /*****************************************************************************
2  * theme_loader.cpp
3  *****************************************************************************
4  * Copyright (C) 2003 the VideoLAN team
5  * $Id$
6  *
7  * Authors: Cyril Deguet     <asmax@via.ecp.fr>
8  *          Olivier Teulière <ipkiss@via.ecp.fr>
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23  *****************************************************************************/
24
25 #include "theme_loader.hpp"
26 #include "theme.hpp"
27 #include "../parser/builder.hpp"
28 #include "../parser/skin_parser.hpp"
29 #include "../src/os_factory.hpp"
30 #include "../src/vlcproc.hpp"
31 #include "../src/window_manager.hpp"
32
33 #include <cctype>
34
35 #ifdef HAVE_FCNTL_H
36 #   include <fcntl.h>
37 #endif
38 #ifdef HAVE_SYS_STAT_H
39 #   include <sys/stat.h>
40 #endif
41 #ifdef HAVE_UNISTD_H
42 #   include <unistd.h>
43 #elif defined( WIN32 ) && !defined( UNDER_CE )
44 #   include <direct.h>
45 #endif
46
47 #ifdef HAVE_DIRENT_H
48 #   include <dirent.h>
49 #endif
50
51
52 #if defined( HAVE_ZLIB_H )
53 #   include <zlib.h>
54 #   include <errno.h>
55 int gzopen_frontend ( const char *pathname, int oflags, int mode );
56 int gzclose_frontend( int );
57 int gzread_frontend ( int, void *, size_t );
58 int gzwrite_frontend( int, const void *, size_t );
59 #if defined( HAVE_LIBTAR_H )
60 #   include <libtar.h>
61 #else
62 typedef gzFile TAR;
63 int tar_open        ( TAR **t, char *pathname, int oflags );
64 int tar_extract_all ( TAR *t, char *prefix );
65 int tar_close       ( TAR *t );
66 int getoct( char *p, int width );
67 #endif
68 int makedir( const char *newdir );
69 #endif
70
71 #define DEFAULT_XML_FILE "theme.xml"
72 #define WINAMP2_XML_FILE "winamp2.xml"
73 #define ZIP_BUFFER_SIZE 4096
74
75
76 bool ThemeLoader::load( const string &fileName )
77 {
78     string path = getFilePath( fileName );
79
80     //Before all, let's see if the file is present
81     struct stat p_stat;
82     if( utf8_stat( path.c_str(), &p_stat ) )
83         return false;
84
85     // First, we try to un-targz the file, and if it fails we hope it's a XML
86     // file...
87
88 #if defined( HAVE_ZLIB_H )
89     if( ! extract( sToLocale( fileName ) ) && ! parse( path, fileName ) )
90         return false;
91 #else
92     if( ! parse( path, fileName ) )
93         return false;
94 #endif
95
96     Theme *pNewTheme = getIntf()->p_sys->p_theme;
97     if( !pNewTheme )
98     {
99         return false;
100     }
101
102     // Check if the skin to load is in the config file, to load its config
103     char *skin_last = config_GetPsz( getIntf(), "skins2-last" );
104     if( skin_last != NULL && fileName == (string)skin_last )
105     {
106         // Restore the theme configuration
107         getIntf()->p_sys->p_theme->loadConfig();
108         // Used to anchor the windows at the beginning
109         pNewTheme->getWindowManager().stopMove();
110     }
111     else
112     {
113         config_PutPsz( getIntf(), "skins2-last", fileName.c_str() );
114         // Show the windows
115         pNewTheme->getWindowManager().showAll( true );
116     }
117     free( skin_last );
118
119     return true;
120 }
121
122
123 #if defined( HAVE_ZLIB_H )
124 bool ThemeLoader::extractTarGz( const string &tarFile, const string &rootDir )
125 {
126     TAR *t;
127 #if defined( HAVE_LIBTAR_H )
128     tartype_t gztype = { (openfunc_t) gzopen_frontend,
129                          (closefunc_t) gzclose_frontend,
130                          (readfunc_t) gzread_frontend,
131                          (writefunc_t) gzwrite_frontend };
132
133     if( tar_open( &t, (char *)tarFile.c_str(), &gztype, O_RDONLY, 0,
134                   TAR_GNU ) == -1 )
135 #else
136     if( tar_open( &t, (char *)tarFile.c_str(), O_RDONLY ) == -1 )
137 #endif
138     {
139         return false;
140     }
141
142     if( tar_extract_all( t, (char *)rootDir.c_str() ) != 0 )
143     {
144         tar_close( t );
145         return false;
146     }
147
148     if( tar_close( t ) != 0 )
149     {
150         return false;
151     }
152
153     return true;
154 }
155
156
157 bool ThemeLoader::extractZip( const string &zipFile, const string &rootDir )
158 {
159     // Try to open the ZIP file
160     unzFile file = unzOpen( zipFile.c_str() );
161     unz_global_info info;
162
163     if( unzGetGlobalInfo( file, &info ) != UNZ_OK )
164     {
165         return false;
166     }
167     // Extract all the files in the archive
168     for( unsigned long i = 0; i < info.number_entry; i++ )
169     {
170         if( !extractFileInZip( file, rootDir ) )
171         {
172             msg_Warn( getIntf(), "error while unzipping %s",
173                       zipFile.c_str() );
174             unzClose( file );
175             return false;
176         }
177
178         if( i < info.number_entry - 1 )
179         {
180             // Go the next file in the archive
181             if( unzGoToNextFile( file ) !=UNZ_OK )
182             {
183                 msg_Warn( getIntf(), "error while unzipping %s",
184                           zipFile.c_str() );
185                 unzClose( file );
186                 return false;
187             }
188         }
189     }
190     unzClose( file );
191     return true;
192 }
193
194
195 bool ThemeLoader::extractFileInZip( unzFile file, const string &rootDir )
196 {
197     // Read info for the current file
198     char filenameInZip[256];
199     unz_file_info fileInfo;
200     if( unzGetCurrentFileInfo( file, &fileInfo, filenameInZip,
201                                sizeof( filenameInZip), NULL, 0, NULL, 0 )
202         != UNZ_OK )
203     {
204         return false;
205     }
206
207 #ifdef WIN32
208
209     // Convert the file name to lower case, because some winamp skins
210     // use the wrong case...
211     for( size_t i=0; i< strlen( filenameInZip ); i++)
212     {
213         filenameInZip[i] = tolower( filenameInZip[i] );
214     }
215
216 #endif
217
218     // Allocate the buffer
219     void *pBuffer = malloc( ZIP_BUFFER_SIZE );
220     if( !pBuffer )
221         return false;
222
223     // Get the path of the file
224     OSFactory *pOsFactory = OSFactory::instance( getIntf() );
225     string fullPath = rootDir
226         + pOsFactory->getDirSeparator()
227         + fixDirSeparators( filenameInZip );
228     string basePath = getFilePath( fullPath );
229
230     // Extract the file if is not a directory
231     if( basePath != fullPath )
232     {
233         if( unzOpenCurrentFile( file ) )
234         {
235             free( pBuffer );
236             return false;
237         }
238         makedir( basePath.c_str() );
239         FILE *fout = fopen( fullPath.c_str(), "wb" );
240         if( fout == NULL )
241         {
242             msg_Err( getIntf(), "error opening %s", fullPath.c_str() );
243             free( pBuffer );
244             return false;
245         }
246
247         // Extract the current file
248         int n;
249         do
250         {
251             n = unzReadCurrentFile( file, pBuffer, ZIP_BUFFER_SIZE );
252             if( n < 0 )
253             {
254                 msg_Err( getIntf(), "error while reading zip file" );
255                 free( pBuffer );
256                 return false;
257             }
258             else if( n > 0 )
259             {
260                 if( fwrite( pBuffer, n , 1, fout) != 1 )
261                 {
262                     msg_Err( getIntf(), "error while writing %s",
263                              fullPath.c_str() );
264                     free( pBuffer );
265                     return false;
266                 }
267             }
268         } while( n > 0 );
269
270         fclose(fout);
271
272         if( unzCloseCurrentFile( file ) != UNZ_OK )
273         {
274             free( pBuffer );
275             return false;
276         }
277     }
278
279     free( pBuffer );
280     return true;
281 }
282
283
284 bool ThemeLoader::extract( const string &fileName )
285 {
286     bool result = true;
287     char *tmpdir = tempnam( NULL, "vlt" );
288     string tempPath = sFromLocale( tmpdir );
289     free( tmpdir );
290
291     // Extract the file in a temporary directory
292     if( ! extractTarGz( fileName, tempPath ) &&
293         ! extractZip( fileName, tempPath ) )
294     {
295         deleteTempFiles( tempPath );
296         return false;
297     }
298
299     string path;
300     string xmlFile;
301     OSFactory *pOsFactory = OSFactory::instance( getIntf() );
302     // Find the XML file in the theme
303     if( findFile( tempPath, DEFAULT_XML_FILE, xmlFile ) )
304     {
305         path = getFilePath( xmlFile );
306     }
307     else
308     {
309         // No XML file, check if it is a winamp2 skin
310         string mainBmp;
311         if( findFile( tempPath, "main.bmp", mainBmp ) )
312         {
313             msg_Dbg( getIntf(), "trying to load a winamp2 skin" );
314             path = getFilePath( mainBmp );
315
316             // Look for winamp2.xml in the resource path
317             list<string> resPath = pOsFactory->getResourcePath();
318             list<string>::const_iterator it;
319             for( it = resPath.begin(); it != resPath.end(); it++ )
320             {
321                 if( findFile( *it, WINAMP2_XML_FILE, xmlFile ) )
322                     break;
323             }
324         }
325     }
326
327     if( !xmlFile.empty() )
328     {
329         // Parse the XML file
330         if (! parse( path, xmlFile ) )
331         {
332             msg_Err( getIntf(), "error while parsing %s", xmlFile.c_str() );
333             result = false;
334         }
335     }
336     else
337     {
338         msg_Err( getIntf(), "no XML found in theme %s", fileName.c_str() );
339         result = false;
340     }
341
342     // Clean-up
343     deleteTempFiles( tempPath );
344     return result;
345 }
346
347
348 void ThemeLoader::deleteTempFiles( const string &path )
349 {
350     OSFactory::instance( getIntf() )->rmDir( path );
351 }
352 #endif // HAVE_ZLIB_H
353
354
355 bool ThemeLoader::parse( const string &path, const string &xmlFile )
356 {
357     // File loaded
358     msg_Dbg( getIntf(), "using skin file: %s", xmlFile.c_str() );
359
360     // Start the parser
361     SkinParser parser( getIntf(), xmlFile, path );
362     if( ! parser.parse() )
363         return false;
364
365     // Build and store the theme
366     Builder builder( getIntf(), parser.getData(), path );
367     getIntf()->p_sys->p_theme = builder.build();
368
369     return true;
370 }
371
372
373 string ThemeLoader::getFilePath( const string &rFullPath )
374 {
375     OSFactory *pOsFactory = OSFactory::instance( getIntf() );
376     const string &sep = pOsFactory->getDirSeparator();
377     // Find the last separator ('/' or '\')
378     string::size_type p = rFullPath.rfind( sep, rFullPath.size() );
379     string basePath;
380     if( p != string::npos )
381     {
382         if( p < rFullPath.size() - 1)
383         {
384             basePath = rFullPath.substr( 0, p );
385         }
386         else
387         {
388             basePath = rFullPath;
389         }
390     }
391     return basePath;
392 }
393
394
395 string ThemeLoader::fixDirSeparators( const string &rPath )
396 {
397     OSFactory *pOsFactory = OSFactory::instance( getIntf() );
398     const string &sep = pOsFactory->getDirSeparator();
399     string::size_type p = rPath.find( "/", 0 );
400     string newPath = rPath;
401     while( p != string::npos )
402     {
403         newPath = newPath.replace( p, 1, sep );
404         p = newPath.find( "/", p + 1 );
405     }
406     return newPath;
407 }
408
409
410 bool ThemeLoader::findFile( const string &rootDir, const string &rFileName,
411                             string &themeFilePath )
412 {
413     // Path separator
414     const string &sep = OSFactory::instance( getIntf() )->getDirSeparator();
415
416     DIR *pCurrDir;
417     char *pszDirContent;
418
419     // Open the dir
420     pCurrDir = utf8_opendir( rootDir.c_str() );
421
422     if( pCurrDir == NULL )
423     {
424         // An error occurred
425         msg_Dbg( getIntf(), "cannot open directory %s", rootDir.c_str() );
426         return false;
427     }
428
429     // While we still have entries in the directory
430     while( ( pszDirContent = utf8_readdir( pCurrDir ) ) != NULL )
431     {
432         string newURI = rootDir + sep + pszDirContent;
433
434         // Skip . and ..
435         if( string( pszDirContent ) != "." &&
436             string( pszDirContent ) != ".." )
437         {
438 #if defined( S_ISDIR )
439             struct stat stat_data;
440
441             if( ( utf8_stat( newURI.c_str(), &stat_data ) == 0 )
442              && S_ISDIR(stat_data.st_mode) )
443 #elif defined( DT_DIR )
444             if( pDirContent->d_type & DT_DIR )
445 #else
446             if( 0 )
447 #endif
448             {
449                 // Can we find the file in this subdirectory?
450                 if( findFile( newURI, rFileName, themeFilePath ) )
451                 {
452                     free( pszDirContent );
453                     closedir( pCurrDir );
454                     return true;
455                 }
456             }
457             else
458             {
459                 // Found the theme file?
460                 if( rFileName == string( pszDirContent ) )
461                 {
462                     themeFilePath = newURI;
463                     free( pszDirContent );
464                     closedir( pCurrDir );
465                     return true;
466                 }
467             }
468         }
469
470         free( pszDirContent );
471     }
472
473     closedir( pCurrDir );
474     return false;
475 }
476
477
478 #if !defined( HAVE_LIBTAR_H ) && defined( HAVE_ZLIB_H )
479
480 /* Values used in typeflag field */
481 #define REGTYPE  '0'            /* regular file */
482 #define AREGTYPE '\0'           /* regular file */
483 #define DIRTYPE  '5'            /* directory */
484
485 #define BLOCKSIZE 512
486
487 struct tar_header
488 {                               /* byte offset */
489     char name[100];             /*   0 */
490     char mode[8];               /* 100 */
491     char uid[8];                /* 108 */
492     char gid[8];                /* 116 */
493     char size[12];              /* 124 */
494     char mtime[12];             /* 136 */
495     char chksum[8];             /* 148 */
496     char typeflag;              /* 156 */
497     char linkname[100];         /* 157 */
498     char magic[6];              /* 257 */
499     char version[2];            /* 263 */
500     char uname[32];             /* 265 */
501     char gname[32];             /* 297 */
502     char devmajor[8];           /* 329 */
503     char devminor[8];           /* 337 */
504     char prefix[155];           /* 345 */
505                                 /* 500 */
506 };
507
508
509 union tar_buffer {
510     char              buffer[BLOCKSIZE];
511     struct tar_header header;
512 };
513
514
515
516 int tar_open( TAR **t, char *pathname, int oflags )
517 {
518     gzFile f = gzopen( pathname, "rb" );
519     if( f == NULL )
520     {
521         fprintf( stderr, "Couldn't gzopen %s\n", pathname );
522         return -1;
523     }
524
525     *t = (gzFile *)malloc( sizeof(gzFile) );
526     **t = f;
527     return 0;
528 }
529
530
531 int tar_extract_all( TAR *t, char *prefix )
532 {
533     union tar_buffer buffer;
534     int   len, err, getheader = 1, remaining = 0;
535     FILE  *outfile = NULL;
536     char  fname[BLOCKSIZE + PATH_MAX];
537
538     while( 1 )
539     {
540         len = gzread( *t, &buffer, BLOCKSIZE );
541         if( len < 0 )
542         {
543             fprintf( stderr, "%s\n", gzerror(*t, &err) );
544         }
545
546         /*
547          * Always expect complete blocks to process
548          * the tar information.
549          */
550         if( len != 0 && len != BLOCKSIZE )
551         {
552             fprintf( stderr, "gzread: incomplete block read\n" );
553             return -1;
554         }
555
556         /*
557          * If we have to get a tar header
558          */
559         if( getheader == 1 )
560         {
561             /*
562              * If we met the end of the tar
563              * or the end-of-tar block, we are done
564              */
565             if( (len == 0) || (buffer.header.name[0] == 0) )
566             {
567                 break;
568             }
569
570             sprintf( fname, "%s/%s", prefix, buffer.header.name );
571
572             /* Check magic value in header */
573             if( strncmp( buffer.header.magic, "GNUtar", 6 ) &&
574                 strncmp( buffer.header.magic, "ustar", 5 ) )
575             {
576                 //fprintf(stderr, "not a tar file\n");
577                 return -1;
578             }
579
580             switch( buffer.header.typeflag )
581             {
582                 case DIRTYPE:
583                     makedir( fname );
584                     break;
585                 case REGTYPE:
586                 case AREGTYPE:
587                     remaining = getoct( buffer.header.size, 12 );
588                     if( remaining )
589                     {
590                         outfile = fopen( fname, "wb" );
591                         if( outfile == NULL )
592                         {
593                             /* try creating directory */
594                             char *p = strrchr( fname, '/' );
595                             if( p != NULL )
596                             {
597                                 *p = '\0';
598                                 makedir( fname );
599                                 *p = '/';
600                                 outfile = fopen( fname, "wb" );
601                                 if( !outfile )
602                                 {
603                                     fprintf( stderr, "tar couldn't create %s\n",
604                                              fname );
605                                 }
606                             }
607                         }
608                     }
609                     else outfile = NULL;
610
611                 /*
612                  * could have no contents
613                  */
614                 getheader = (remaining) ? 0 : 1;
615                 break;
616             default:
617                 break;
618             }
619         }
620         else
621         {
622             unsigned int bytes = (remaining > BLOCKSIZE)?BLOCKSIZE:remaining;
623
624             if( outfile != NULL )
625             {
626                 if( fwrite( &buffer, sizeof(char), bytes, outfile ) != bytes )
627                 {
628                     fprintf( stderr, "error writing %s skipping...\n", fname );
629                     fclose( outfile );
630                     outfile = NULL;
631                     unlink( fname );
632                 }
633             }
634             remaining -= bytes;
635             if( remaining == 0 )
636             {
637                 getheader = 1;
638                 if( outfile != NULL )
639                 {
640                     fclose(outfile);
641                     outfile = NULL;
642                 }
643             }
644         }
645     }
646
647     return 0;
648 }
649
650
651 int tar_close( TAR *t )
652 {
653     if( gzclose( *t ) != Z_OK ) fprintf( stderr, "failed gzclose\n" );
654     free( t );
655     return 0;
656 }
657
658
659 /* helper functions */
660 int getoct( char *p, int width )
661 {
662     int result = 0;
663     char c;
664
665     while( width-- )
666     {
667         c = *p++;
668         if( c == ' ' )
669             continue;
670         if( c == 0 )
671             break;
672         result = result * 8 + (c - '0');
673     }
674     return result;
675 }
676
677 #endif
678
679 #ifdef WIN32
680 #  define mkdir(dirname,mode) _mkdir(dirname)
681 #endif
682
683 /* Recursive make directory
684  * Abort if you get an ENOENT errno somewhere in the middle
685  * e.g. ignore error "mkdir on existing directory"
686  *
687  * return 1 if OK, 0 on error
688  */
689 int makedir( const char *newdir )
690 {
691     char *p, *buffer = strdup( newdir );
692     int  len = strlen( buffer );
693
694     if( len <= 0 )
695     {
696         free( buffer );
697         return 0;
698     }
699
700     if( buffer[len-1] == '/' )
701     {
702         buffer[len-1] = '\0';
703     }
704
705     if( mkdir( buffer, 0775 ) == 0 )
706     {
707         free( buffer );
708         return 1;
709     }
710
711     p = buffer + 1;
712     while( 1 )
713     {
714         char hold;
715
716         while( *p && *p != '\\' && *p != '/' ) p++;
717         hold = *p;
718         *p = 0;
719         if( ( mkdir( buffer, 0775 ) == -1 ) && ( errno == ENOENT ) )
720         {
721             fprintf( stderr, "couldn't create directory %s\n", buffer );
722             free( buffer );
723             return 0;
724         }
725         if( hold == 0 ) break;
726         *p++ = hold;
727     }
728     free( buffer );
729     return 1;
730 }
731
732 #ifdef HAVE_ZLIB_H
733
734 static int currentGzFd = -1;
735 static void * currentGzVp = NULL;
736
737 int gzopen_frontend( const char *pathname, int oflags, int mode )
738 {
739     const char *gzflags;
740     gzFile gzf;
741
742     switch( oflags )
743     {
744         case O_WRONLY:
745             gzflags = "wb";
746             break;
747         case O_RDONLY:
748             gzflags = "rb";
749             break;
750         case O_RDWR:
751         default:
752             errno = EINVAL;
753             return -1;
754     }
755
756     gzf = gzopen( pathname, gzflags );
757     if( !gzf )
758     {
759         errno = ENOMEM;
760         return -1;
761     }
762
763     /** Hum ... */
764     currentGzFd = 42;
765     currentGzVp = gzf;
766
767     return currentGzFd;
768 }
769
770 int gzclose_frontend( int fd )
771 {
772     if( currentGzVp != NULL && fd != -1 )
773     {
774         void *toClose = currentGzVp;
775         currentGzVp = NULL;  currentGzFd = -1;
776         return gzclose( toClose );
777     }
778     return -1;
779 }
780
781 int gzread_frontend( int fd, void *p_buffer, size_t i_length )
782 {
783     if( currentGzVp != NULL && fd != -1 )
784     {
785         return gzread( currentGzVp, p_buffer, i_length );
786     }
787     return -1;
788 }
789
790 int gzwrite_frontend( int fd, const void * p_buffer, size_t i_length )
791 {
792     if( currentGzVp != NULL && fd != -1 )
793     {
794         return gzwrite( currentGzVp, const_cast<void*>(p_buffer), i_length );
795     }
796     return -1;
797 }
798
799 #endif