├── requirements.txt ├── .gitignore ├── appimager ├── core ├── xdg │ ├── __init__.py │ ├── Config.py │ ├── Exceptions.py │ ├── Locale.py │ ├── BaseDirectory.py │ ├── RecentFiles.py │ ├── IniFile.py │ ├── IconTheme.py │ ├── Mime.py │ ├── DesktopEntry.py │ ├── MenuEditor.py │ └── Menu.py ├── app.py ├── data.py └── xdgappdir.py ├── docker ├── etc │ ├── pacman.conf │ └── pacman.list └── Dockerfile ├── cli ├── base.py ├── destroy.py ├── status.py ├── stop.py ├── start.py ├── setup.py ├── install.py └── package.py ├── AppImage.yml ├── README.md ├── LICENSE └── runtime ├── CMakeLists.txt ├── linux ├── rock.h └── iso_fs.h ├── md5.h ├── fuseiso.c ├── isofs.h ├── runtime.c └── md5.c /requirements.txt: -------------------------------------------------------------------------------- 1 | cement==2.8.2 2 | docker-py==1.8.1 3 | PyYAML==3.11 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.cmake 3 | *.a 4 | runtime/runtime 5 | Makefile 6 | CMakeCache.txt 7 | CMakeFiles 8 | -------------------------------------------------------------------------------- /appimager: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from core import app 3 | 4 | with app.AppImager() as cli: 5 | cli.run() 6 | -------------------------------------------------------------------------------- /core/xdg/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] 2 | -------------------------------------------------------------------------------- /docker/etc/pacman.conf: -------------------------------------------------------------------------------- 1 | [options] 2 | Architecture = auto 3 | IgnorePkg = filesystem 4 | 5 | [core] 6 | Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch 7 | 8 | [community] 9 | Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch 10 | 11 | [extra] 12 | Server = http://mirror.rackspace.com/archlinux/$repo/os/$arch 13 | -------------------------------------------------------------------------------- /cli/base.py: -------------------------------------------------------------------------------- 1 | from cement.core.controller import CementBaseController, expose 2 | 3 | class BaseController(CementBaseController): 4 | class Meta: 5 | label = 'base' 6 | description = "AppImager provides a build environment and tools for managing AppImage dependencies, assisting in the creation of AppDir's and creating AppImages from source code." 7 | -------------------------------------------------------------------------------- /cli/destroy.py: -------------------------------------------------------------------------------- 1 | from cli import base 2 | from cement.core.controller import CementBaseController, expose 3 | 4 | class DestroyController(CementBaseController): 5 | class Meta: 6 | label = 'destroy' 7 | stacked_on = 'base' 8 | 9 | @expose(help='Destroys the Docker container for this environment.') 10 | def destroy(self): 11 | self.app.log.info("Destroy command") 12 | -------------------------------------------------------------------------------- /cli/status.py: -------------------------------------------------------------------------------- 1 | from cli import base 2 | from cement.core.controller import CementBaseController, expose 3 | 4 | class StatusController(CementBaseController): 5 | class Meta: 6 | label = 'status' 7 | stacked_on = 'base' 8 | 9 | @expose(help='Shows the status of the Docker container for this environment.') 10 | def status(self): 11 | self.app.log.info("Status command") 12 | -------------------------------------------------------------------------------- /AppImage.yml: -------------------------------------------------------------------------------- 1 | name: AppImager 2 | description: AppImager manages AppImage dependencies, assists in the creation of AppDir's and creates AppImages from source code 3 | 4 | build: cmake . && make clean && make 5 | 6 | require: 7 | fuse: 2.9.6-1 8 | zlib: 1.2.8-4 9 | 10 | require_build: 11 | cmake: 3.5.2-2 12 | binutils: 2.26-4 13 | glibc: 2.23-4 14 | glib2: 2.48.1-1 15 | gcc: 6.1.1-1 16 | -------------------------------------------------------------------------------- /docker/etc/pacman.list: -------------------------------------------------------------------------------- 1 | bash 2 | bzip2 3 | coreutils 4 | curl 5 | device-mapper 6 | e2fsprogs 7 | file 8 | filesystem 9 | findutils 10 | gawk 11 | gettext 12 | grep 13 | gzip 14 | inetutils 15 | iptables 16 | iputils 17 | less 18 | nano 19 | netctl 20 | pacman 21 | procps-ng 22 | psmisc 23 | sed 24 | shadow 25 | systemd 26 | systemd-sysvcompat 27 | tar 28 | texinfo 29 | usbutils 30 | util-linux 31 | which 32 | xz 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AppImager 2 | 3 | AppImager is a CLI tool for creating and managing [AppImages](http://appimage.org/). 4 | 5 | It has the ability to manage application dependences, setup an AppDir and package that AppDir into an AppImage. 6 | 7 | ## Dependencies 8 | 9 | - cmake 10 | - binutils 11 | - docker 12 | - fuse 13 | - glibc 14 | - glib2 15 | - gcc 16 | - zlib 17 | - xorriso 18 | 19 | ## Building 20 | 21 | ```bash 22 | cmake . 23 | make clean 24 | make 25 | ``` 26 | -------------------------------------------------------------------------------- /core/app.py: -------------------------------------------------------------------------------- 1 | from cement.core.foundation import CementApp 2 | from cli import base, setup, start, stop, destroy, status, install, package 3 | 4 | class AppImager(CementApp): 5 | class Meta: 6 | label = 'appimager' 7 | base_controller = 'base' 8 | handlers = [ 9 | base.BaseController, 10 | setup.SetupController, 11 | start.StartController, 12 | stop.StopController, 13 | destroy.DestroyController, 14 | status.StatusController, 15 | install.InstallController, 16 | package.PackageController 17 | ] 18 | -------------------------------------------------------------------------------- /cli/stop.py: -------------------------------------------------------------------------------- 1 | from cli import base 2 | from docker import Client 3 | from core import data 4 | from cement.core.controller import CementBaseController, expose 5 | 6 | class StopController(CementBaseController): 7 | class Meta: 8 | label = 'stop' 9 | stacked_on = 'base' 10 | 11 | @expose(help='Stops the Docker container for this environment.') 12 | def stop(self): 13 | data_obj = data.Data() 14 | container_name = data_obj.get_path_hash() 15 | 16 | docker = Client() 17 | 18 | print('Stopping container...') 19 | 20 | docker.stop(container_name) 21 | 22 | print("Container stopped") 23 | -------------------------------------------------------------------------------- /cli/start.py: -------------------------------------------------------------------------------- 1 | from cli import base 2 | from docker import Client 3 | from core import data 4 | from cement.core.controller import CementBaseController, expose 5 | 6 | class StartController(CementBaseController): 7 | class Meta: 8 | label = 'start' 9 | stacked_on = 'base' 10 | 11 | @expose(help='Starts the Docker container for this environment.') 12 | def start(self): 13 | data_obj = data.Data() 14 | container_name = data_obj.get_path_hash() 15 | 16 | docker = Client() 17 | 18 | print('Starting container...') 19 | 20 | docker.start(container_name) 21 | 22 | print("Container started") 23 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM base/archlinux:latest 2 | MAINTAINER Nimbusoft Ltd 3 | ADD etc /etc 4 | CMD /bin/bash 5 | RUN rm -rf /etc/pacman.d/gnupg && \ 6 | pacman-key --init && \ 7 | pacman-key --populate archlinux && \ 8 | pacman -Sy --noconfirm archlinux-keyring && \ 9 | pacman -Syy && \ 10 | pacman -S --noconfirm pacman && \ 11 | pacman-db-upgrade && \ 12 | pacman -Suu --noconfirm && \ 13 | pacman -S --asdeps --noconfirm $(pacman -Qq) && \ 14 | pacman -S --asexplicit --noconfirm $(cat /etc/pacman.list) && \ 15 | pacman -Rcns --noconfirm $(pacman -Qdqt) && \ 16 | pacman -Scc && \ 17 | pacman -Sy --needed --noconfirm base-devel && \ 18 | find /etc/ -type f -name '*.pac*' -exec rm {} \; && \ 19 | rm -rf /var/cache/pacman/pkg/* && \ 20 | pacman -S --noconfirm xorriso 21 | -------------------------------------------------------------------------------- /core/xdg/Config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions to configure Basic Settings 3 | """ 4 | 5 | language = "C" 6 | windowmanager = None 7 | icon_theme = "highcolor" 8 | icon_size = 48 9 | cache_time = 5 10 | root_mode = False 11 | 12 | def setWindowManager(wm): 13 | global windowmanager 14 | windowmanager = wm 15 | 16 | def setIconTheme(theme): 17 | global icon_theme 18 | icon_theme = theme 19 | import xdg.IconTheme 20 | xdg.IconTheme.themes = [] 21 | 22 | def setIconSize(size): 23 | global icon_size 24 | icon_size = size 25 | 26 | def setCacheTime(time): 27 | global cache_time 28 | cache_time = time 29 | 30 | def setLocale(lang): 31 | import locale 32 | lang = locale.normalize(lang) 33 | locale.setlocale(locale.LC_ALL, lang) 34 | import xdg.Locale 35 | xdg.Locale.update(lang) 36 | 37 | def setRootMode(boolean): 38 | global root_mode 39 | root_mode = boolean 40 | -------------------------------------------------------------------------------- /core/data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | import yaml 4 | import platform 5 | 6 | class Data: 7 | def get_work_path(self): 8 | return self.get_path('work') 9 | 10 | def get_out_path(self): 11 | return self.get_path('out') 12 | 13 | def get_path_hash(self, key=''): 14 | return hashlib.md5(self.get_path(key).encode('utf-8')).hexdigest() 15 | 16 | def get_path(self, key=''): 17 | cwd = os.getcwd() 18 | default = cwd + "/" + key 19 | 20 | yaml_key = key + '_path' 21 | yaml_data = self.get_yml_data() 22 | 23 | if yaml_key in yaml_data: 24 | return cwd + '/' + yaml_data[yaml_key] 25 | 26 | return default 27 | 28 | def get_yml_data(self): 29 | file_path = os.getcwd() + "/AppImage.yml" 30 | 31 | stream = open(file_path, 'r') 32 | 33 | return yaml.load(stream) 34 | 35 | def architecture(self): 36 | arch = platform.architecture()[0] 37 | 38 | if arch == '64bit': 39 | return 'x86_64' 40 | 41 | return 'i686' 42 | -------------------------------------------------------------------------------- /cli/setup.py: -------------------------------------------------------------------------------- 1 | from cli import base 2 | from core import data 3 | from docker import Client 4 | from cement.core.controller import CementBaseController, expose 5 | import json 6 | import os 7 | 8 | class SetupController(CementBaseController): 9 | class Meta: 10 | label = 'setup' 11 | stacked_on = 'base' 12 | 13 | @expose(help='Sets up a Docker container for this environment.') 14 | def setup(self): 15 | data_obj = data.Data() 16 | path = data_obj.get_work_path() 17 | 18 | docker = Client() 19 | 20 | print('Setting up environment, please wait...') 21 | 22 | volume = os.getcwd() 23 | 24 | container_name = data_obj.get_path_hash() 25 | 26 | docker.create_container('nimbusoft/appimager', tty=True, command="/bin/bash", name=container_name, volumes=['/mnt/appimager'], 27 | host_config=docker.create_host_config(binds={ 28 | os.getcwd(): { 29 | 'bind': '/mnt/appimager', 30 | 'mode': 'rw', 31 | } 32 | })) 33 | 34 | docker.start(container_name) 35 | print('Setup Complete') 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nimbusoft Ltd 4 | Copyright (c) 2004-16 Simon Peter 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /core/xdg/Exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exception Classes for the xdg package 3 | """ 4 | 5 | debug = False 6 | 7 | class Error(Exception): 8 | def __init__(self, msg): 9 | self.msg = msg 10 | Exception.__init__(self, msg) 11 | def __str__(self): 12 | return self.msg 13 | 14 | class ValidationError(Error): 15 | def __init__(self, msg, file): 16 | self.msg = msg 17 | self.file = file 18 | Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) 19 | 20 | class ParsingError(Error): 21 | def __init__(self, msg, file): 22 | self.msg = msg 23 | self.file = file 24 | Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) 25 | 26 | class NoKeyError(Error): 27 | def __init__(self, key, group, file): 28 | Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) 29 | self.key = key 30 | self.group = group 31 | 32 | class DuplicateKeyError(Error): 33 | def __init__(self, key, group, file): 34 | Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) 35 | self.key = key 36 | self.group = group 37 | 38 | class NoGroupError(Error): 39 | def __init__(self, group, file): 40 | Error.__init__(self, "No group: %s in file %s" % (group, file)) 41 | self.group = group 42 | 43 | class DuplicateGroupError(Error): 44 | def __init__(self, group, file): 45 | Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) 46 | self.group = group 47 | 48 | class NoThemeError(Error): 49 | def __init__(self, theme): 50 | Error.__init__(self, "No such icon-theme: %s" % theme) 51 | self.theme = theme 52 | -------------------------------------------------------------------------------- /cli/install.py: -------------------------------------------------------------------------------- 1 | from cli import base 2 | from core import data 3 | import os 4 | import sys 5 | from urllib.request import urlretrieve 6 | from cement.core.controller import CementBaseController, expose 7 | 8 | class InstallController(CementBaseController): 9 | class Meta: 10 | label = 'install' 11 | stacked_on = 'base' 12 | 13 | @expose(help='Installs dependencies from an AppImage.yml file.') 14 | def install(self): 15 | data_obj = data.Data() 16 | yaml = data_obj.get_yml_data() 17 | 18 | arch = data_obj.architecture() 19 | 20 | if not os.path.exists("build"): 21 | print("Creating build directory") 22 | os.mkdir("build") 23 | 24 | print("Downloading app dependencies...") 25 | 26 | for package, version in yaml['require'].items(): 27 | url = "https://archive.archlinux.org/packages/" + package[0] + "/" + package + "/" + package + "-" + version + "-" + arch + ".pkg.tar.xz" 28 | 29 | def reporthook(blocknum, blocksize, totalsize): 30 | readsofar = blocknum * blocksize 31 | if totalsize > 0: 32 | percent = readsofar * 1e2 / totalsize 33 | s = "\rDownloading " + package + " (" + version + ") %5.1f%% %*d / %dK" % ( 34 | percent, len(str(totalsize)), readsofar / 1024, totalsize / 1024) 35 | sys.stderr.write(s) 36 | if readsofar >= totalsize: # near the end 37 | sys.stderr.write("\n") 38 | else: # total size is unknown 39 | sys.stderr.write("read %d\n" % (readsofar,)) 40 | 41 | urlretrieve(url, "build/" + package + ".tar.xz", reporthook) 42 | 43 | print("Complete") 44 | -------------------------------------------------------------------------------- /core/xdg/Locale.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper Module for Locale settings 3 | 4 | This module is based on a ROX module (LGPL): 5 | 6 | http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log 7 | """ 8 | 9 | import os 10 | from locale import normalize 11 | 12 | regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?" 13 | 14 | def _expand_lang(locale): 15 | locale = normalize(locale) 16 | COMPONENT_CODESET = 1 << 0 17 | COMPONENT_MODIFIER = 1 << 1 18 | COMPONENT_TERRITORY = 1 << 2 19 | # split up the locale into its base components 20 | mask = 0 21 | pos = locale.find('@') 22 | if pos >= 0: 23 | modifier = locale[pos:] 24 | locale = locale[:pos] 25 | mask |= COMPONENT_MODIFIER 26 | else: 27 | modifier = '' 28 | pos = locale.find('.') 29 | codeset = '' 30 | if pos >= 0: 31 | locale = locale[:pos] 32 | pos = locale.find('_') 33 | if pos >= 0: 34 | territory = locale[pos:] 35 | locale = locale[:pos] 36 | mask |= COMPONENT_TERRITORY 37 | else: 38 | territory = '' 39 | language = locale 40 | ret = [] 41 | for i in range(mask+1): 42 | if not (i & ~mask): # if all components for this combo exist ... 43 | val = language 44 | if i & COMPONENT_TERRITORY: val += territory 45 | if i & COMPONENT_CODESET: val += codeset 46 | if i & COMPONENT_MODIFIER: val += modifier 47 | ret.append(val) 48 | ret.reverse() 49 | return ret 50 | 51 | def expand_languages(languages=None): 52 | # Get some reasonable defaults for arguments that were not supplied 53 | if languages is None: 54 | languages = [] 55 | for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): 56 | val = os.environ.get(envar) 57 | if val: 58 | languages = val.split(':') 59 | break 60 | #if 'C' not in languages: 61 | # languages.append('C') 62 | 63 | # now normalize and expand the languages 64 | nelangs = [] 65 | for lang in languages: 66 | for nelang in _expand_lang(lang): 67 | if nelang not in nelangs: 68 | nelangs.append(nelang) 69 | return nelangs 70 | 71 | def update(language=None): 72 | global langs 73 | if language: 74 | langs = expand_languages([language]) 75 | else: 76 | langs = expand_languages() 77 | 78 | langs = [] 79 | update() 80 | -------------------------------------------------------------------------------- /runtime/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | CMAKE_MINIMUM_REQUIRED(VERSION 2.6) 2 | 3 | PROJECT(AppImageKit C) 4 | 5 | # Set required libraries. 6 | set(LIBFUSE "fuse") 7 | set(LIBPTHREAD "pthread") 8 | set(LIBGLIB2 "glib-2.0") 9 | set(LIBZ "z") 10 | 11 | SET(REQUIRED_LIBRARIES ${LIBFUSE} ${LIBPTHREAD}) 12 | foreach(LIB ${REQUIRED_LIBRARIES}) 13 | find_library(FOUND${LIB} ${LIB} PATHS "/lib64" "/usr/lib64") 14 | if (NOT FOUND${LIB}) 15 | message(FATAL_ERROR "The required library '${LIB}' was not found. Please install it on your system first.") 16 | endif(NOT FOUND${LIB}) 17 | endforeach(LIB) 18 | 19 | ADD_DEFINITIONS(-g -O2 -D_FILE_OFFSET_BITS=64) 20 | 21 | # Begin find glib 22 | 23 | if (GLIB_PKG_FOUND) 24 | find_path(GLIB_INCLUDE_DIR NAMES glib.h PATH_SUFFIXES glib-2.0 25 | PATHS 26 | ${GLIB_PKG_INCLUDE_DIRS} 27 | /usr/include/glib-2.0 28 | /usr/include 29 | /usr/local/include 30 | ) 31 | find_path(GLIB_CONFIG_INCLUDE_DIR NAMES glibconfig.h PATHS ${GLIB_PKG_LIBDIR} PATH_SUFFIXES glib-2.0/include) 32 | 33 | find_library(GLIB_LIBRARIES NAMES glib-2.0 34 | PATHS 35 | ${GLIB_PKG_LIBRARY_DIRS} 36 | /usr/lib 37 | /usr/local/lib 38 | ) 39 | 40 | else (GLIB_PKG_FOUND) 41 | # Find Glib even if pkg-config is not working (eg. cross compiling to Windows) 42 | find_library(GLIB_LIBRARIES NAMES glib-2.0) 43 | string (REGEX REPLACE "/[^/]*$" "" GLIB_LIBRARIES_DIR ${GLIB_LIBRARIES}) 44 | 45 | find_path(GLIB_INCLUDE_DIR NAMES glib.h PATH_SUFFIXES glib-2.0) 46 | find_path(GLIB_CONFIG_INCLUDE_DIR NAMES glibconfig.h PATHS ${GLIB_LIBRARIES_DIR} PATH_SUFFIXES glib-2.0/include) 47 | 48 | endif (GLIB_PKG_FOUND) 49 | 50 | if (GLIB_INCLUDE_DIR AND GLIB_CONFIG_INCLUDE_DIR AND GLIB_LIBRARIES) 51 | set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIR} ${GLIB_CONFIG_INCLUDE_DIR}) 52 | endif (GLIB_INCLUDE_DIR AND GLIB_CONFIG_INCLUDE_DIR AND GLIB_LIBRARIES) 53 | 54 | if(GLIB_INCLUDE_DIRS AND GLIB_LIBRARIES) 55 | set(GLIB_FOUND TRUE CACHE INTERNAL "glib-2.0 found") 56 | message(STATUS "Found glib-2.0: ${GLIB_INCLUDE_DIR}, ${GLIB_LIBRARIES}") 57 | else(GLIB_INCLUDE_DIRS AND GLIB_LIBRARIES) 58 | set(GLIB_FOUND FALSE CACHE INTERNAL "glib-2.0 found") 59 | message(STATUS "glib-2.0 not found.") 60 | endif(GLIB_INCLUDE_DIRS AND GLIB_LIBRARIES) 61 | 62 | mark_as_advanced(GLIB_INCLUDE_DIR GLIB_CONFIG_INCLUDE_DIR GLIB_INCLUDE_DIRS GLIB_LIBRARIES) 63 | 64 | # End find glib 65 | 66 | INCLUDE_DIRECTORIES(. ${GLIB_INCLUDE_DIRS}) 67 | 68 | ADD_LIBRARY(fuseiso fuseiso.c) 69 | ADD_LIBRARY(isofs isofs.c) 70 | 71 | ADD_EXECUTABLE(runtime runtime.c) 72 | TARGET_LINK_LIBRARIES(runtime fuseiso isofs ${LIBFUSE} ${LIBPTHREAD} ${LIBGLIB2} ${LIBZ}) 73 | add_custom_command(TARGET runtime POST_BUILD COMMAND ${CMAKE_STRIP} runtime) 74 | -------------------------------------------------------------------------------- /cli/package.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from cli import base 3 | from core import data 4 | from cement.core.controller import CementBaseController, expose 5 | import os, sys 6 | import subprocess 7 | from core import xdgappdir 8 | from locale import gettext as _ 9 | 10 | class PackageController(CementBaseController): 11 | class Meta: 12 | label = 'package' 13 | stacked_on = 'base' 14 | 15 | @expose(help='Package the AppDir into an AppImage.') 16 | def package(self): 17 | data_obj = data.Data() 18 | path = data_obj.get_work_path() 19 | 20 | # Also search for dependency binaries and libraries next to myself 21 | dependenciesdir = path + "/usr/" 22 | os.environ['PATH'] = dependenciesdir + "/bin:" + os.getenv('PATH') 23 | # print os.environ['PATH'] 24 | lddp = os.getenv('LD_LIBRARY_PATH') 25 | if lddp == None: lddp = "" 26 | os.environ['LD_LIBRARY_PATH'] = dependenciesdir + "/lib:" + lddp 27 | 28 | sourcedir = path 29 | destinationfile = data_obj.get_out_path() 30 | should_compress = True 31 | 32 | if should_compress == True: 33 | if not os.path.exists(sourcedir): 34 | print("Application work directory not found: %s" % (sourcedir)) 35 | exit(1) 36 | 37 | if should_compress == True: 38 | H = xdgappdir.AppDirXdgHandler(sourcedir) 39 | iconfile = H.get_icon_path_by_icon_name(H.get_icon_name_from_desktop_file(H.desktopfile)) 40 | if iconfile == None: 41 | print("Icon could not be found based on information in desktop file") 42 | #exit(1) 43 | 44 | print("Creating %s..." % (destinationfile)) 45 | if os.path.exists(destinationfile): 46 | print (_("Destination path already exists, exiting")) # xorriso would append another session to a pre-existing image 47 | exit(1) 48 | # As great as xorriso is, as cryptic its usage is :-( 49 | command = ["xorriso", "-joliet", "on", "-volid", "AppImage", "-dev", 50 | destinationfile, "-padding", "0", "-map", 51 | sourcedir, "/", "--", "-map", iconfile, "/.DirIcon", 52 | "-zisofs", "level=9:block_size=128k:by_magic=off", "-chown_r", "0", 53 | "/", "--", "set_filter_r", "--zisofs", "/" ] 54 | 55 | subprocess.Popen(command).communicate() 56 | 57 | print("ok") 58 | 59 | print("Embedding runtime...") 60 | elf = os.path.realpath(os.path.dirname(__file__)) + "/runtime" 61 | s = open(elf, 'rb') 62 | f = open(destinationfile, 'rb+') 63 | f.write(bytes(s.read())) 64 | f.close() 65 | s.close() 66 | print("ok") 67 | 68 | print("Making %s executable..." % (destinationfile)) 69 | 70 | os.chmod(destinationfile, 0o755) 71 | 72 | print("ok") 73 | 74 | filesize = int(os.stat(destinationfile).st_size) 75 | print (_("Size: %f MB") % (filesize/1024/1024)) 76 | -------------------------------------------------------------------------------- /runtime/linux/rock.h: -------------------------------------------------------------------------------- 1 | // this file was borrowed from linux kernel 2 | // originally it has name /usr/src/linux/fs/isofs/rock.h 3 | // unfortunately there was no copyright header on it, 4 | // so i`m assume it have copyrighted under terms of 5 | // GNU GENERAL PUBLIC LICENSE version 2 6 | // as a whole linux kernel 7 | // copy of this license included into fuseiso 8 | // distribution in file COPYING 9 | 10 | 11 | /* These structs are used by the system-use-sharing protocol, in which the 12 | Rock Ridge extensions are embedded. It is quite possible that other 13 | extensions are present on the disk, and this is fine as long as they 14 | all use SUSP */ 15 | 16 | struct SU_SP{ 17 | unsigned char magic[2]; 18 | unsigned char skip; 19 | } __attribute__((packed)); 20 | 21 | struct SU_CE{ 22 | char extent[8]; 23 | char offset[8]; 24 | char size[8]; 25 | }; 26 | 27 | struct SU_ER{ 28 | unsigned char len_id; 29 | unsigned char len_des; 30 | unsigned char len_src; 31 | unsigned char ext_ver; 32 | char data[0]; 33 | } __attribute__((packed)); 34 | 35 | struct RR_RR{ 36 | char flags[1]; 37 | } __attribute__((packed)); 38 | 39 | struct RR_PX{ 40 | char mode[8]; 41 | char n_links[8]; 42 | char uid[8]; 43 | char gid[8]; 44 | }; 45 | 46 | struct RR_PN{ 47 | char dev_high[8]; 48 | char dev_low[8]; 49 | }; 50 | 51 | 52 | struct SL_component{ 53 | unsigned char flags; 54 | unsigned char len; 55 | char text[0]; 56 | } __attribute__((packed)); 57 | 58 | struct RR_SL{ 59 | unsigned char flags; 60 | struct SL_component link; 61 | } __attribute__((packed)); 62 | 63 | struct RR_NM{ 64 | unsigned char flags; 65 | char name[0]; 66 | } __attribute__((packed)); 67 | 68 | struct RR_CL{ 69 | char location[8]; 70 | }; 71 | 72 | struct RR_PL{ 73 | char location[8]; 74 | }; 75 | 76 | struct stamp{ 77 | char time[7]; 78 | } __attribute__((packed)); 79 | 80 | struct RR_TF{ 81 | char flags; 82 | struct stamp times[0]; /* Variable number of these beasts */ 83 | } __attribute__((packed)); 84 | 85 | /* Linux-specific extension for transparent decompression */ 86 | struct RR_ZF{ 87 | char algorithm[2]; 88 | char parms[2]; 89 | char real_size[8]; 90 | }; 91 | 92 | /* These are the bits and their meanings for flags in the TF structure. */ 93 | #define TF_CREATE 1 94 | #define TF_MODIFY 2 95 | #define TF_ACCESS 4 96 | #define TF_ATTRIBUTES 8 97 | #define TF_BACKUP 16 98 | #define TF_EXPIRATION 32 99 | #define TF_EFFECTIVE 64 100 | #define TF_LONG_FORM 128 101 | 102 | struct rock_ridge{ 103 | char signature[2]; 104 | unsigned char len; 105 | unsigned char version; 106 | union{ 107 | struct SU_SP SP; 108 | struct SU_CE CE; 109 | struct SU_ER ER; 110 | struct RR_RR RR; 111 | struct RR_PX PX; 112 | struct RR_PN PN; 113 | struct RR_SL SL; 114 | struct RR_NM NM; 115 | struct RR_CL CL; 116 | struct RR_PL PL; 117 | struct RR_TF TF; 118 | struct RR_ZF ZF; 119 | } u; 120 | }; 121 | 122 | #define RR_PX 1 /* POSIX attributes */ 123 | #define RR_PN 2 /* POSIX devices */ 124 | #define RR_SL 4 /* Symbolic link */ 125 | #define RR_NM 8 /* Alternate Name */ 126 | #define RR_CL 16 /* Child link */ 127 | #define RR_PL 32 /* Parent link */ 128 | #define RR_RE 64 /* Relocation directory */ 129 | #define RR_TF 128 /* Timestamps */ 130 | -------------------------------------------------------------------------------- /core/xdg/BaseDirectory.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is based on a rox module (LGPL): 3 | 4 | http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log 5 | 6 | The freedesktop.org Base Directory specification provides a way for 7 | applications to locate shared data and configuration: 8 | 9 | http://standards.freedesktop.org/basedir-spec/ 10 | 11 | (based on version 0.6) 12 | 13 | This module can be used to load and save from and to these directories. 14 | 15 | Typical usage: 16 | 17 | from rox import basedir 18 | 19 | for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): 20 | print "Load settings from", dir 21 | 22 | dir = basedir.save_config_path('mydomain.org', 'MyProg') 23 | print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" 24 | 25 | Note: see the rox.Options module for a higher-level API for managing options. 26 | """ 27 | 28 | from __future__ import generators 29 | import os 30 | 31 | _home = os.environ.get('HOME', '/') 32 | xdg_data_home = os.environ.get('XDG_DATA_HOME', 33 | os.path.join(_home, '.local', 'share')) 34 | 35 | xdg_data_dirs = [xdg_data_home] + \ 36 | os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':') 37 | 38 | xdg_config_home = os.environ.get('XDG_CONFIG_HOME', 39 | os.path.join(_home, '.config')) 40 | 41 | xdg_config_dirs = [xdg_config_home] + \ 42 | os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':') 43 | 44 | xdg_cache_home = os.environ.get('XDG_CACHE_HOME', 45 | os.path.join(_home, '.cache')) 46 | 47 | xdg_data_dirs = filter(lambda x: x, xdg_data_dirs) 48 | xdg_config_dirs = filter(lambda x: x, xdg_config_dirs) 49 | 50 | def save_config_path(*resource): 51 | """Ensure $XDG_CONFIG_HOME// exists, and return its path. 52 | 'resource' should normally be the name of your application. Use this 53 | when SAVING configuration settings. Use the xdg_config_dirs variable 54 | for loading.""" 55 | resource = os.path.join(*resource) 56 | assert not resource.startswith('/') 57 | path = os.path.join(xdg_config_home, resource) 58 | if not os.path.isdir(path): 59 | os.makedirs(path, 0o700) 60 | return path 61 | 62 | def save_data_path(*resource): 63 | """Ensure $XDG_DATA_HOME// exists, and return its path. 64 | 'resource' is the name of some shared resource. Use this when updating 65 | a shared (between programs) database. Use the xdg_data_dirs variable 66 | for loading.""" 67 | resource = os.path.join(*resource) 68 | assert not resource.startswith('/') 69 | path = os.path.join(xdg_data_home, resource) 70 | if not os.path.isdir(path): 71 | os.makedirs(path) 72 | return path 73 | 74 | def load_config_paths(*resource): 75 | """Returns an iterator which gives each directory named 'resource' in the 76 | configuration search path. Information provided by earlier directories should 77 | take precedence over later ones (ie, the user's config dir comes first).""" 78 | resource = os.path.join(*resource) 79 | for config_dir in xdg_config_dirs: 80 | path = os.path.join(config_dir, resource) 81 | if os.path.exists(path): yield path 82 | 83 | def load_first_config(*resource): 84 | """Returns the first result from load_config_paths, or None if there is nothing 85 | to load.""" 86 | for x in load_config_paths(*resource): 87 | return x 88 | return None 89 | 90 | def load_data_paths(*resource): 91 | """Returns an iterator which gives each directory named 'resource' in the 92 | shared data search path. Information provided by earlier directories should 93 | take precedence over later ones.""" 94 | resource = os.path.join(*resource) 95 | for data_dir in xdg_data_dirs: 96 | path = os.path.join(data_dir, resource) 97 | if os.path.exists(path): yield path 98 | -------------------------------------------------------------------------------- /runtime/md5.h: -------------------------------------------------------------------------------- 1 | /* md5.h - Declaration of functions and data types used for MD5 sum 2 | computing library functions. 3 | Copyright (C) 1995 Free Software Foundation, Inc. 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2, or (at your option) 8 | any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ 18 | 19 | #ifndef _MD5_H 20 | #define _MD5_H 21 | 22 | #include 23 | 24 | #if defined HAVE_LIMITS_H || _LIBC 25 | # include 26 | #endif 27 | 28 | /* The following contortions are an attempt to use the C preprocessor 29 | to determine an unsigned integral type that is 32 bits wide. An 30 | alternative approach is to use autoconf's AC_CHECK_SIZEOF macro, but 31 | doing that would require that the configure script compile and *run* 32 | the resulting executable. Locally running cross-compiled executables 33 | is usually not possible. */ 34 | 35 | #if defined __STDC__ && __STDC__ 36 | # define UINT_MAX_32_BITS 4294967295U 37 | #else 38 | # define UINT_MAX_32_BITS 0xFFFFFFFF 39 | #endif 40 | 41 | /* If UINT_MAX isn't defined, assume it's a 32-bit type. 42 | This should be valid for all systems GNU cares about because 43 | that doesn't include 16-bit systems, and only modern systems 44 | (that certainly have ) have 64+-bit integral types. */ 45 | 46 | #ifndef UINT_MAX 47 | # define UINT_MAX UINT_MAX_32_BITS 48 | #endif 49 | 50 | #if UINT_MAX == UINT_MAX_32_BITS 51 | typedef unsigned int md5_uint32; 52 | #else 53 | # if USHRT_MAX == UINT_MAX_32_BITS 54 | typedef unsigned short md5_uint32; 55 | # else 56 | # if ULONG_MAX == UINT_MAX_32_BITS 57 | typedef unsigned long md5_uint32; 58 | # else 59 | /* The following line is intended to evoke an error. 60 | Using #error is not portable enough. */ 61 | "Cannot determine unsigned 32-bit data type." 62 | # endif 63 | # endif 64 | #endif 65 | 66 | #undef __P 67 | #if defined (__STDC__) && __STDC__ 68 | #define __P(x) x 69 | #else 70 | #define __P(x) () 71 | #endif 72 | 73 | /* Structure to save state of computation between the single steps. */ 74 | struct md5_ctx 75 | { 76 | md5_uint32 A; 77 | md5_uint32 B; 78 | md5_uint32 C; 79 | md5_uint32 D; 80 | }; 81 | 82 | /* 83 | * The following three functions are build up the low level used in 84 | * the functions `md5_stream' and `md5_buffer'. 85 | */ 86 | 87 | /* Initialize structure containing state of computation. 88 | (RFC 1321, 3.3: Step 3) */ 89 | void md5_init_ctx __P ((struct md5_ctx *ctx)); 90 | 91 | /* Starting with the result of former calls of this function (or the 92 | initialzation function update the context for the next LEN bytes 93 | starting at BUFFER. 94 | It is necessary that LEN is a multiple of 64!!! */ 95 | void md5_process_block __P ((const void *buffer, size_t len, 96 | struct md5_ctx *ctx)); 97 | 98 | /* Put result from CTX in first 16 bytes following RESBUF. The result is 99 | always in little endian byte order, so that a byte-wise output yields 100 | to the wanted ASCII representation of the message digest. */ 101 | void *md5_read_ctx __P ((const struct md5_ctx *ctx, void *resbuf)); 102 | 103 | 104 | /* Compute MD5 message digest for bytes read from STREAM. The 105 | resulting message digest number will be written into the 16 bytes 106 | beginning at RESBLOCK. */ 107 | int md5_stream __P ((FILE *stream, void *resblock)); 108 | 109 | /* Compute MD5 message digest for LEN bytes beginning at BUFFER. The 110 | result is always in little endian byte order, so that a byte-wise 111 | output yields to the wanted ASCII representation of the message 112 | digest. */ 113 | void *md5_buffer __P ((const char *buffer, size_t len, void *resblock)); 114 | 115 | #endif -------------------------------------------------------------------------------- /core/xdgappdir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # /************************************************************************** 4 | # 5 | # Copyright (c) 2004-16 Simon Peter 6 | # 7 | # All Rights Reserved. 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in 17 | # all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | # THE SOFTWARE. 26 | # 27 | # **************************************************************************/ 28 | 29 | import os, sys, subprocess, glob 30 | import xdg.IconTheme, xdg.DesktopEntry # apt-get install python-xdg, present on Ubuntu 31 | from locale import gettext as _ 32 | 33 | def get_status_output(*args, **kwargs): 34 | p = subprocess.Popen(*args, **kwargs) 35 | stdout, stderr = p.communicate() 36 | return p.returncode, stdout, stderr 37 | 38 | class AppDirXdgHandler(object): 39 | 40 | def __init__(self, appdir): 41 | self.appdir = appdir 42 | # print _("AppDir: %s" % [self.appdir]) 43 | newicondirs = [appdir] 44 | for icondir in xdg.IconTheme.icondirs: 45 | if icondir.startswith(xdg.IconTheme.basedir): 46 | icondir = self.appdir + icondir 47 | newicondirs.append(icondir) 48 | xdg.IconTheme.icondirs = newicondirs + xdg.IconTheme.icondirs # search AppDir, then system 49 | self.desktopfile = self._find_desktop_file_in_appdir() 50 | try: self.name = self.get_nice_name_from_desktop_file(self.desktopfile) 51 | except: self.name = "Unknown" 52 | try: self.executable = self.get_exec_path(self.get_exec_name_from_desktop_file(self.desktopfile)) 53 | except: self.executable = None 54 | try: self.icon = self.get_icon_path_by_icon_name(self.get_icon_name_from_desktop_file(self.desktopfile)) 55 | except: self.icon = None 56 | 57 | def __repr__(self): 58 | try: return("%s AppDir %s with desktop file %s, executable %s and icon %s" % (self.name, self.appdir, self.desktopfile, self.executable, self.icon)) 59 | except: return("AppDir") 60 | 61 | def get_icon_path_by_icon_name(self, icon_name): 62 | """Return the path of the icon for a given icon name""" 63 | for format in ["png", "xpm"] : 64 | icon = xdg.IconTheme.getIconPath(icon_name, 48, None, format) 65 | if icon != None : 66 | return icon 67 | return None # If we have not found an icon 68 | 69 | def _find_all_executables_in_appdir(self): 70 | """Return all executable files in the AppDir, or None""" 71 | results = [] 72 | result, executables = get_status_output("find " + self.appdir + " -type f -perm -u+x") 73 | executables = executables.split("\n") 74 | if result != 0: 75 | return None 76 | for executable in executables: 77 | if os.path.basename(executable) != "AppRun": 78 | results.append(executable) 79 | return results 80 | 81 | def _find_desktop_file_in_appdir(self): 82 | try: 83 | result = glob.glob(self.appdir + '/*.desktop')[0] 84 | return result 85 | except: 86 | return None 87 | 88 | def get_icon_name_from_desktop_file(self, desktop_file): 89 | "Returns the Icon= entry of a given desktop file" 90 | icon_name = os.path.basename(xdg.DesktopEntry.DesktopEntry(filename=desktop_file).getIcon()) 91 | # print icon_name 92 | return icon_name 93 | 94 | def get_exec_name_from_desktop_file(self, desktop_file): 95 | "Returns the Exec= entry of a given desktop file" 96 | exec_name = os.path.basename(xdg.DesktopEntry.DesktopEntry(filename=desktop_file).getExec()).replace(" %u","").replace(" %U","").replace(" %f","").replace(" %F","") 97 | # print exec_name 98 | return exec_name 99 | 100 | def get_nice_name_from_desktop_file(self, desktop_file): 101 | "Returns the Name= entry of a given desktop file" 102 | name = xdg.DesktopEntry.DesktopEntry(filename=desktop_file).getName() 103 | return name 104 | 105 | def get_exec_path(self, exec_name): 106 | results = [] 107 | for excp in ["", "bin", "sbin", "usr/bin", "usr/sbin", "usr/games"]: 108 | trial = os.path.join(self.appdir, excp, exec_name) 109 | if os.path.exists(trial): 110 | return trial 111 | return None 112 | 113 | 114 | if __name__ == "__main__": 115 | if len(sys.argv) < 2: 116 | print (_("Usage: %s AppDir" % (sys.argv[0]))) 117 | exit(1) 118 | appdir = sys.argv[1] 119 | H = AppDirXdgHandler(appdir) 120 | print(H) 121 | 122 | -------------------------------------------------------------------------------- /core/xdg/RecentFiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the XDG Recent File Storage Specification Version 0.2 3 | http://standards.freedesktop.org/recent-file-spec 4 | """ 5 | 6 | import xml.dom.minidom, xml.sax.saxutils 7 | import os, time, fcntl 8 | from xdg.Exceptions import * 9 | 10 | class RecentFiles: 11 | def __init__(self): 12 | self.RecentFiles = [] 13 | self.filename = "" 14 | 15 | def parse(self, filename=None): 16 | if not filename: 17 | filename = os.path.join(os.getenv("HOME"), ".recently-used") 18 | 19 | try: 20 | doc = xml.dom.minidom.parse(filename) 21 | except IOError: 22 | raise ParsingError('File not found', filename) 23 | except xml.parsers.expat.ExpatError: 24 | raise ParsingError('Not a valid .menu file', filename) 25 | 26 | self.filename = filename 27 | 28 | for child in doc.childNodes: 29 | if child.nodeType == xml.dom.Node.ELEMENT_NODE: 30 | if child.tagName == "RecentFiles": 31 | for recent in child.childNodes: 32 | if recent.nodeType == xml.dom.Node.ELEMENT_NODE: 33 | if recent.tagName == "RecentItem": 34 | self.__parseRecentItem(recent) 35 | 36 | self.sort() 37 | 38 | def __parseRecentItem(self, item): 39 | recent = RecentFile() 40 | self.RecentFiles.append(recent) 41 | 42 | for attribute in item.childNodes: 43 | if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: 44 | if attribute.tagName == "URI": 45 | recent.URI = attribute.childNodes[0].nodeValue 46 | elif attribute.tagName == "Mime-Type": 47 | recent.MimeType = attribute.childNodes[0].nodeValue 48 | elif attribute.tagName == "Timestamp": 49 | recent.Timestamp = attribute.childNodes[0].nodeValue 50 | elif attribute.tagName == "Private": 51 | recent.Prviate = True 52 | elif attribute.tagName == "Groups": 53 | 54 | for group in attribute.childNodes: 55 | if group.nodeType == xml.dom.Node.ELEMENT_NODE: 56 | if group.tagName == "Group": 57 | recent.Groups.append(group.childNodes[0].nodeValue) 58 | 59 | def write(self, filename=None): 60 | if not filename and not self.filename: 61 | raise ParsingError('File not found', filename) 62 | elif not filename: 63 | filename = self.filename 64 | 65 | f = open(filename, "w") 66 | fcntl.lockf(f, fcntl.LOCK_EX) 67 | f.write('\n') 68 | f.write("\n") 69 | 70 | for r in self.RecentFiles: 71 | f.write(" \n") 72 | f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) 73 | f.write(" %s\n" % r.MimeType) 74 | f.write(" %s\n" % r.Timestamp) 75 | if r.Private == True: 76 | f.write(" \n") 77 | if len(r.Groups) > 0: 78 | f.write(" \n") 79 | for group in r.Groups: 80 | f.write(" %s\n" % group) 81 | f.write(" \n") 82 | f.write(" \n") 83 | 84 | f.write("\n") 85 | fcntl.lockf(f, fcntl.LOCK_UN) 86 | f.close() 87 | 88 | def getFiles(self, mimetypes=None, groups=None, limit=0): 89 | tmp = [] 90 | i = 0 91 | for item in self.RecentFiles: 92 | if groups: 93 | for group in groups: 94 | if group in item.Groups: 95 | tmp.append(item) 96 | i += 1 97 | elif mimetypes: 98 | for mimetype in mimetypes: 99 | if mimetype == item.MimeType: 100 | tmp.append(item) 101 | i += 1 102 | else: 103 | if item.Private == False: 104 | tmp.append(item) 105 | i += 1 106 | if limit != 0 and i == limit: 107 | break 108 | 109 | return tmp 110 | 111 | def addFile(self, item, mimetype, groups=None, private=False): 112 | # check if entry already there 113 | if item in self.RecentFiles: 114 | index = self.RecentFiles.index(item) 115 | recent = self.RecentFiles[index] 116 | else: 117 | # delete if more then 500 files 118 | if len(self.RecentFiles) == 500: 119 | self.RecentFiles.pop() 120 | # add entry 121 | recent = RecentFile() 122 | self.RecentFiles.append(recent) 123 | 124 | recent.URI = item 125 | recent.MimeType = mimetype 126 | recent.Timestamp = int(time.time()) 127 | recent.Private = private 128 | recent.Groups = groups 129 | 130 | self.sort() 131 | 132 | def deleteFile(self, item): 133 | if item in self.RecentFiles: 134 | self.RecentFiles.remove(item) 135 | 136 | def sort(self): 137 | self.RecentFiles.sort() 138 | self.RecentFiles.reverse() 139 | 140 | 141 | class RecentFile: 142 | def __init__(self): 143 | self.URI = "" 144 | self.MimeType = "" 145 | self.Timestamp = "" 146 | self.Private = False 147 | self.Groups = [] 148 | 149 | def __cmp__(self, other): 150 | return cmp(self.Timestamp, other.Timestamp) 151 | 152 | def __eq__(self, other): 153 | if self.URI == str(other): 154 | return True 155 | else: 156 | return False 157 | 158 | def __str__(self): 159 | return self.URI 160 | -------------------------------------------------------------------------------- /runtime/fuseiso.c: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | * Copyright (c) 2005, 2006 by Dmitry Morozhnikov * 3 | * Copyright (c) 2004-16 Simon Peter * 4 | * * 5 | * This program is free software; you can redistribute it and/or modify * 6 | * it under the terms of the GNU General Public License as published by * 7 | * the Free Software Foundation; either version 2 of the License, or * 8 | * (at your option) any later version. * 9 | * * 10 | * This program is distributed in the hope that it will be useful, * 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 13 | * GNU General Public License for more details. * 14 | * * 15 | * You should have received a copy of the GNU General Public License * 16 | * along with this program; if not, write to the * 17 | * Free Software Foundation, Inc., * 18 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * 19 | ***************************************************************************/ 20 | 21 | #ifdef HAVE_CONFIG_H 22 | #include 23 | #endif 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #define FUSE_USE_VERSION 22 36 | #include 37 | #include "isofs.h" 38 | 39 | #ifdef __GNUC__ 40 | # define UNUSED(x) x __attribute__((unused)) 41 | #else 42 | # define UNUSED(x) x 43 | #endif 44 | 45 | static char *imagefile = NULL; 46 | static char *mount_point = NULL; 47 | static int image_fd = -1; 48 | 49 | int maintain_mount_point = 1; 50 | 51 | char* iocharset; 52 | 53 | char* normalize_name(const char* fname) { 54 | char* abs_fname = (char *) malloc(PATH_MAX); 55 | realpath(fname, abs_fname); 56 | // ignore errors from realpath() 57 | return abs_fname; 58 | }; 59 | 60 | int check_mount_point() { 61 | struct stat st; 62 | int rc = lstat(mount_point, &st); 63 | if(rc == -1 && errno == ENOENT) { 64 | // directory does not exists, createcontext 65 | rc = mkdir(mount_point, 0777); // let`s underlying filesystem manage permissions 66 | if(rc != 0) { 67 | perror("Can't create mount point"); 68 | return -EIO; 69 | }; 70 | } else if(rc == -1) { 71 | perror("Can't check mount point"); 72 | return -1; 73 | }; 74 | return 0; 75 | }; 76 | 77 | void del_mount_point() { 78 | int rc = rmdir(mount_point); 79 | if(rc != 0) { 80 | perror("Can't delete mount point"); 81 | }; 82 | }; 83 | 84 | static int isofs_getattr(const char *path, struct stat *stbuf) 85 | { 86 | return isofs_real_getattr(path, stbuf); 87 | } 88 | 89 | static int isofs_readlink(const char *path, char *target, size_t size) { 90 | return isofs_real_readlink(path, target, size); 91 | }; 92 | 93 | static int isofs_open(const char *path, struct fuse_file_info *UNUSED(fi)) 94 | { 95 | return isofs_real_open(path); 96 | } 97 | 98 | static int isofs_read(const char *path, char *buf, size_t size, 99 | off_t offset, struct fuse_file_info *UNUSED(fi)) 100 | { 101 | return isofs_real_read(path, buf, size, offset); 102 | } 103 | 104 | static int isofs_flush(const char *UNUSED(path), struct fuse_file_info *UNUSED(fi)) { 105 | return 0; 106 | }; 107 | 108 | static void* isofs_init() { 109 | int rc; 110 | run_when_fuse_fs_mounted(); 111 | return isofs_real_init(); 112 | }; 113 | 114 | static void isofs_destroy(void* param) { 115 | return; 116 | }; 117 | 118 | static int isofs_opendir(const char *path, struct fuse_file_info *UNUSED(fi)) { 119 | return isofs_real_opendir(path); 120 | }; 121 | 122 | static int isofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t UNUSED(offset), 123 | struct fuse_file_info *UNUSED(fi)) { 124 | return isofs_real_readdir(path, buf, filler); 125 | }; 126 | 127 | static int isofs_statfs(const char *UNUSED(path), struct statfs *stbuf) 128 | { 129 | return isofs_real_statfs(stbuf); 130 | } 131 | 132 | static struct fuse_operations isofs_oper = { 133 | .getattr = isofs_getattr, 134 | .readlink = isofs_readlink, 135 | .open = isofs_open, 136 | .read = isofs_read, 137 | .flush = isofs_flush, 138 | .init = isofs_init, 139 | .destroy = isofs_destroy, 140 | .opendir = isofs_opendir, 141 | .readdir = isofs_readdir, 142 | .statfs = isofs_statfs, 143 | }; 144 | 145 | int ext2_main(int argc, char *argv[]) 146 | { 147 | imagefile = normalize_name(argv[0]); 148 | image_fd = open(imagefile, O_RDONLY); 149 | if(image_fd == -1) { 150 | perror("Can't open image file"); 151 | fprintf(stderr, "Supplied image file name: \"%s\"\n", imagefile); 152 | exit(EXIT_FAILURE); 153 | }; 154 | 155 | mount_point = normalize_name(argv[1]); 156 | 157 | if(!iocharset) { 158 | iocharset = "UTF-8//IGNORE"; 159 | }; 160 | 161 | int rc; 162 | if(maintain_mount_point) { 163 | rc = check_mount_point(); 164 | if(rc != 0) { 165 | exit(EXIT_FAILURE); 166 | }; 167 | }; 168 | if(maintain_mount_point) { 169 | rc = atexit(del_mount_point); 170 | if(rc != 0) { 171 | fprintf(stderr, "Can't set exit function\n"); 172 | exit(EXIT_FAILURE); 173 | } 174 | }; 175 | 176 | // will exit in case of failure 177 | rc = isofs_real_preinit(imagefile, image_fd); 178 | 179 | return fuse_main(argc, argv, &isofs_oper); 180 | }; 181 | 182 | -------------------------------------------------------------------------------- /runtime/isofs.h: -------------------------------------------------------------------------------- 1 | /*************************************************************************** 2 | * Copyright (c) 2005, 2006 by Dmitry Morozhnikov * 3 | * * 4 | * This program is free software; you can redistribute it and/or modify * 5 | * it under the terms of the GNU General Public License as published by * 6 | * the Free Software Foundation; either version 2 of the License, or * 7 | * (at your option) any later version. * 8 | * * 9 | * This program is distributed in the hope that it will be useful, * 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 12 | * GNU General Public License for more details. * 13 | * * 14 | * You should have received a copy of the GNU General Public License * 15 | * along with this program; if not, write to the * 16 | * Free Software Foundation, Inc., * 17 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * 18 | ***************************************************************************/ 19 | 20 | #ifndef _ISOFS_H 21 | #define _ISOFS_H 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | typedef int (*isofs_dir_fill_t) (void *buf, const char *name, 28 | const struct stat *stat, off_t off); 29 | 30 | typedef struct _isofs_context { 31 | char *imagefile; 32 | int fd; 33 | int pd_have_rr; // 1 if primary descriptor have hierarchy with rrip extension 34 | struct iso_primary_descriptor pd; 35 | int supplementary; // 1 if supplementary descriptor found and in effect 36 | struct iso_supplementary_descriptor sd; 37 | struct iso_directory_record *root; 38 | int file_offset; // offset to begin of useful data (for .nrg files) 39 | int id_offset; // offset to CD001 inside file 40 | size_t block_size; // raw block size 41 | size_t block_offset; // offset from block start to data 42 | size_t data_size; // data size inside block 43 | int susp; // parse susp entries 44 | int susp_skip; // skip bytes from susp SP entry 45 | int joliet_level; // joliet extension level (1, 2 or 3) 46 | ino_t last_ino; 47 | } isofs_context; 48 | 49 | typedef struct _isofs_inode { 50 | struct iso_directory_record *record; 51 | struct stat st; 52 | ino_t st_ino; 53 | time_t ctime; // cached value from record->date 54 | char *sl; 55 | size_t sl_len; 56 | char *nm; 57 | size_t nm_len; 58 | int cl_block; 59 | int pl_block; 60 | size_t zf_size; 61 | size_t real_size; 62 | char zf_algorithm[2]; 63 | size_t zf_header_size; 64 | int zf_block_shift; 65 | int zf_nblocks; 66 | int *zf_blockptr; 67 | int PX; // 1 if PX entry found, st in effect 68 | int SL; // 1 if SL entry found, sl in effect 69 | int NM; // 1 if NM entry found, nm in effect 70 | int CL; // 1 if CL found, cl_block in effect 71 | int PL; // 1 if PL found, pl_block in effect 72 | int RE; 73 | int TF; // 1 if TF entry found, st in effect 74 | int ZF; // 1 if ZF entry found 75 | } isofs_inode; 76 | 77 | // borrowed from zisofs-tools 78 | typedef struct _zf_file_header { 79 | char magic[8]; 80 | char uncompressed_len[4]; 81 | unsigned char header_size; 82 | unsigned char block_size; 83 | char reserved[2]; 84 | } zf_file_header; 85 | 86 | // macros for iso_directory_record->flags 87 | #define ISO_FLAGS_HIDDEN(x) (*((unsigned char *) x) & 1) 88 | #define ISO_FLAGS_DIR(x) (*((unsigned char *) x) & (1 << 1)) 89 | 90 | // borrowed from linux kernel rock ridge code 91 | 92 | #define SIG(A,B) ((A) | ((B) << 8)) /* isonum_721() */ 93 | 94 | // borrowed from linux kernel isofs code 95 | 96 | /* Number conversion inlines, named after the section in ISO 9660 97 | they correspond to. */ 98 | 99 | #include 100 | 101 | static inline int isonum_711(unsigned char *p) 102 | { 103 | return *(unsigned char *)p; 104 | } 105 | // static inline int isonum_712(char *p) 106 | // { 107 | // return *(s8 *)p; 108 | // } 109 | static inline unsigned int isonum_721(char *p) 110 | { 111 | #if defined(WORDS_BIGENDIAN) 112 | return *(unsigned short *)p; 113 | #else 114 | return bswap_16(*(unsigned short *)p); 115 | #endif 116 | } 117 | // static inline unsigned int isonum_722(char *p) 118 | // { 119 | // return be16_to_cpu(get_unaligned((__le16 *)p)); 120 | // } 121 | static inline unsigned int isonum_723(char *p) 122 | { 123 | /* Ignore bigendian datum due to broken mastering programs */ 124 | #if defined(WORDS_BIGENDIAN) 125 | return bswap_16(*(unsigned short *)p); 126 | #else 127 | return *(unsigned short *)p; 128 | #endif 129 | } 130 | static inline unsigned int isonum_731(char *p) 131 | { 132 | #if defined(WORDS_BIGENDIAN) 133 | return bswap_32(*(unsigned int *)p); 134 | #else 135 | return *(unsigned int *)p; 136 | #endif 137 | } 138 | // static inline unsigned int isonum_732(char *p) 139 | // { 140 | // return be32_to_cpu(get_unaligned((__le32 *)p)); 141 | // } 142 | static inline unsigned int isonum_733(char *p) 143 | { 144 | /* Ignore bigendian datum due to broken mastering programs */ 145 | #if defined(WORDS_BIGENDIAN) 146 | return bswap_32(*(unsigned int *)p); 147 | #else 148 | return *(unsigned int *)p; 149 | #endif 150 | } 151 | 152 | int isofs_real_preinit(char* imagefile, int fd); 153 | void* isofs_real_init(); 154 | 155 | int isofs_real_opendir(const char *path); 156 | int isofs_real_readdir(const char *path, void *filler_buf, isofs_dir_fill_t filler); 157 | int isofs_real_getattr(const char *path, struct stat *stbuf); 158 | int isofs_real_readlink(const char *path, char *target, size_t size); 159 | int isofs_real_open(const char *path); 160 | int isofs_real_read(const char *path, char *out_buf, size_t size, off_t offset); 161 | int isofs_real_statfs(struct statfs *stbuf); 162 | 163 | #endif // _ISOFS_H 164 | -------------------------------------------------------------------------------- /runtime/runtime.c: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | 3 | Copyright (c) 2016 Nimbusoft Ltd 4 | Copyright (c) 2004-16 Simon Peter 5 | Copyright (c) 2007 Alexander Larsson 6 | 7 | All Rights Reserved. 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | 27 | **************************************************************************/ 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | 41 | /* ======================================================== Start helper functions for icon extraction */ 42 | /* 43 | Constructs the name of the thumbnail image for $HOME/.thumbnails for the executable that is itself 44 | See http://people.freedesktop.org/~vuntz/thumbnail-spec-cache/ 45 | Partly borrowed from 46 | http://www.google.com/codesearch#n76pnUnMG18/trunk/blender/imbuf/intern/thumbs.c&q=.thumbnails/normal%20lang:c%20md5&type=cs 47 | */ 48 | 49 | #include "md5.h" 50 | #include "md5.c" 51 | #include 52 | #include 53 | 54 | #define FILE_MAX 240 55 | #define URI_MAX FILE_MAX*3 + 8 56 | 57 | /* --- begin of adapted code from glib --- 58 | * The following code is adapted from function g_escape_uri_string from the gnome glib 59 | * Source: http://svn.gnome.org/viewcvs/glib/trunk/glib/gconvert.c?view=markup 60 | * released under the Gnu General Public License. 61 | * NOTE THIS DOESN'T WORK PROPERLY FOR öäüß - FIXME 62 | */ 63 | 64 | typedef enum { 65 | UNSAFE_ALL = 0x1, /* Escape all unsafe characters */ 66 | UNSAFE_ALLOW_PLUS = 0x2, /* Allows '+' */ 67 | UNSAFE_PATH = 0x8, /* Allows '/', '&', '=', ':', '@', '+', '$' and ',' */ 68 | UNSAFE_HOST = 0x10, /* Allows '/' and ':' and '@' */ 69 | UNSAFE_SLASHES = 0x20 /* Allows all characters except for '/' and '%' */ 70 | } UnsafeCharacterSet; 71 | 72 | static const unsigned char acceptable[96] = { 73 | /* A table of the ASCII chars from space (32) to DEL (127) */ 74 | 0x00,0x3F,0x20,0x20,0x28,0x00,0x2C,0x3F,0x3F,0x3F,0x3F,0x2A,0x28,0x3F,0x3F,0x1C, 75 | 0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x38,0x20,0x20,0x2C,0x20,0x20, 76 | 0x38,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F, 77 | 0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x20,0x20,0x20,0x20,0x3F, 78 | 0x20,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F, 79 | 0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x3F,0x20,0x20,0x20,0x3F,0x20 80 | }; 81 | 82 | static const char hex[17] = "0123456789abcdef"; 83 | 84 | void escape_uri_string (const char *string, char* escaped_string, int len,UnsafeCharacterSet mask) 85 | { 86 | #define ACCEPTABLE(a) ((a)>=32 && (a)<128 && (acceptable[(a)-32] & use_mask)) 87 | 88 | const char *p; 89 | char *q; 90 | int c; 91 | UnsafeCharacterSet use_mask; 92 | use_mask = mask; 93 | 94 | for (q = escaped_string, p = string; (*p != '\0') && len; p++) { 95 | c = (unsigned char) *p; 96 | len--; 97 | 98 | if (!ACCEPTABLE (c)) { 99 | *q++ = '%'; /* means hex coming */ 100 | *q++ = hex[c >> 4]; 101 | *q++ = hex[c & 15]; 102 | } else { 103 | *q++ = *p; 104 | } 105 | } 106 | 107 | *q = '\0'; 108 | } 109 | 110 | void to_hex_char(char* hexbytes, const unsigned char* bytes, int len) 111 | { 112 | const unsigned char *p; 113 | char *q; 114 | 115 | for (q = hexbytes, p = bytes; len; p++) { 116 | const unsigned char c = (unsigned char) *p; 117 | len--; 118 | *q++ = hex[c >> 4]; 119 | *q++ = hex[c & 15]; 120 | } 121 | } 122 | 123 | /* --- end of adapted code from glib --- */ 124 | 125 | static int uri_from_filename( const char *dir, char *newuri ) 126 | { 127 | char uri[URI_MAX]; 128 | sprintf (uri, "file://%s", dir); 129 | char newstring[URI_MAX]; 130 | strncpy(newstring, uri, URI_MAX); 131 | newstring[URI_MAX - 1] = 0; 132 | unsigned int i = 0; 133 | escape_uri_string(newstring, newuri, FILE_MAX*3+8, UNSAFE_PATH); 134 | return 1; 135 | } 136 | 137 | 138 | static void thumbname_from_uri(const char* uri, char* thumb) 139 | { 140 | char hexdigest[33]; 141 | unsigned char digest[16]; 142 | md5_buffer( uri, strlen(uri), digest); 143 | hexdigest[0] = '\0'; 144 | to_hex_char(hexdigest, digest, 16); 145 | hexdigest[32] = '\0'; 146 | sprintf(thumb, "%s.png", hexdigest); 147 | 148 | } 149 | 150 | /* ======================================================== End helper functions for icon extraction */ 151 | 152 | extern int ext2_main(int argc, char *argv[], void (*mounted) (void)); 153 | extern void ext2_quit(void); 154 | 155 | static pid_t fuse_pid; 156 | static int keepalive_pipe[2]; 157 | 158 | static void * 159 | write_pipe_thread (void *arg) 160 | { 161 | char c[32]; 162 | int res; 163 | // sprintf(stderr, "Called write_pipe_thread"); 164 | memset (c, 'x', sizeof (c)); 165 | while (1) { 166 | /* Write until we block, on broken pipe, exit */ 167 | res = write (keepalive_pipe[1], c, sizeof (c)); 168 | if (res == -1) { 169 | kill (fuse_pid, SIGHUP); 170 | break; 171 | } 172 | } 173 | return NULL; 174 | } 175 | 176 | void 177 | run_when_fuse_fs_mounted (void) 178 | { 179 | 180 | // sprintf(stderr, "Called run_when_fuse_fs_mounted"); 181 | pthread_t thread; 182 | int res; 183 | 184 | fuse_pid = getpid(); 185 | res = pthread_create(&thread, NULL, write_pipe_thread, keepalive_pipe); 186 | } 187 | 188 | char* getArg(int argc, char *argv[],char chr) 189 | { 190 | int i; 191 | for (i=1; i 15 | /* 16 | * The isofs filesystem constants/structures 17 | */ 18 | 19 | /* This part borrowed from the bsd386 isofs */ 20 | #define ISODCL(from, to) (to - from + 1) 21 | 22 | struct iso_volume_descriptor { 23 | char type[ISODCL(1,1)]; /* 711 */ 24 | char id[ISODCL(2,6)]; 25 | char version[ISODCL(7,7)]; 26 | char data[ISODCL(8,2048)]; 27 | }; 28 | 29 | /* volume descriptor types */ 30 | #define ISO_VD_PRIMARY 1 31 | #define ISO_VD_SUPPLEMENTARY 2 32 | #define ISO_VD_END 255 33 | 34 | #define ISO_STANDARD_ID "CD001" 35 | 36 | struct iso_primary_descriptor { 37 | char type [ISODCL ( 1, 1)]; /* 711 */ 38 | char id [ISODCL ( 2, 6)]; 39 | char version [ISODCL ( 7, 7)]; /* 711 */ 40 | char unused1 [ISODCL ( 8, 8)]; 41 | char system_id [ISODCL ( 9, 40)]; /* achars */ 42 | char volume_id [ISODCL ( 41, 72)]; /* dchars */ 43 | char unused2 [ISODCL ( 73, 80)]; 44 | char volume_space_size [ISODCL ( 81, 88)]; /* 733 */ 45 | char unused3 [ISODCL ( 89, 120)]; 46 | char volume_set_size [ISODCL (121, 124)]; /* 723 */ 47 | char volume_sequence_number [ISODCL (125, 128)]; /* 723 */ 48 | char logical_block_size [ISODCL (129, 132)]; /* 723 */ 49 | char path_table_size [ISODCL (133, 140)]; /* 733 */ 50 | char type_l_path_table [ISODCL (141, 144)]; /* 731 */ 51 | char opt_type_l_path_table [ISODCL (145, 148)]; /* 731 */ 52 | char type_m_path_table [ISODCL (149, 152)]; /* 732 */ 53 | char opt_type_m_path_table [ISODCL (153, 156)]; /* 732 */ 54 | char root_directory_record [ISODCL (157, 190)]; /* 9.1 */ 55 | char volume_set_id [ISODCL (191, 318)]; /* dchars */ 56 | char publisher_id [ISODCL (319, 446)]; /* achars */ 57 | char preparer_id [ISODCL (447, 574)]; /* achars */ 58 | char application_id [ISODCL (575, 702)]; /* achars */ 59 | char copyright_file_id [ISODCL (703, 739)]; /* 7.5 dchars */ 60 | char abstract_file_id [ISODCL (740, 776)]; /* 7.5 dchars */ 61 | char bibliographic_file_id [ISODCL (777, 813)]; /* 7.5 dchars */ 62 | char creation_date [ISODCL (814, 830)]; /* 8.4.26.1 */ 63 | char modification_date [ISODCL (831, 847)]; /* 8.4.26.1 */ 64 | char expiration_date [ISODCL (848, 864)]; /* 8.4.26.1 */ 65 | char effective_date [ISODCL (865, 881)]; /* 8.4.26.1 */ 66 | char file_structure_version [ISODCL (882, 882)]; /* 711 */ 67 | char unused4 [ISODCL (883, 883)]; 68 | char application_data [ISODCL (884, 1395)]; 69 | char unused5 [ISODCL (1396, 2048)]; 70 | }; 71 | 72 | /* Almost the same as the primary descriptor but two fields are specified */ 73 | struct iso_supplementary_descriptor { 74 | char type [ISODCL ( 1, 1)]; /* 711 */ 75 | char id [ISODCL ( 2, 6)]; 76 | char version [ISODCL ( 7, 7)]; /* 711 */ 77 | char flags [ISODCL ( 8, 8)]; /* 853 */ 78 | char system_id [ISODCL ( 9, 40)]; /* achars */ 79 | char volume_id [ISODCL ( 41, 72)]; /* dchars */ 80 | char unused2 [ISODCL ( 73, 80)]; 81 | char volume_space_size [ISODCL ( 81, 88)]; /* 733 */ 82 | char escape [ISODCL ( 89, 120)]; /* 856 */ 83 | char volume_set_size [ISODCL (121, 124)]; /* 723 */ 84 | char volume_sequence_number [ISODCL (125, 128)]; /* 723 */ 85 | char logical_block_size [ISODCL (129, 132)]; /* 723 */ 86 | char path_table_size [ISODCL (133, 140)]; /* 733 */ 87 | char type_l_path_table [ISODCL (141, 144)]; /* 731 */ 88 | char opt_type_l_path_table [ISODCL (145, 148)]; /* 731 */ 89 | char type_m_path_table [ISODCL (149, 152)]; /* 732 */ 90 | char opt_type_m_path_table [ISODCL (153, 156)]; /* 732 */ 91 | char root_directory_record [ISODCL (157, 190)]; /* 9.1 */ 92 | char volume_set_id [ISODCL (191, 318)]; /* dchars */ 93 | char publisher_id [ISODCL (319, 446)]; /* achars */ 94 | char preparer_id [ISODCL (447, 574)]; /* achars */ 95 | char application_id [ISODCL (575, 702)]; /* achars */ 96 | char copyright_file_id [ISODCL (703, 739)]; /* 7.5 dchars */ 97 | char abstract_file_id [ISODCL (740, 776)]; /* 7.5 dchars */ 98 | char bibliographic_file_id [ISODCL (777, 813)]; /* 7.5 dchars */ 99 | char creation_date [ISODCL (814, 830)]; /* 8.4.26.1 */ 100 | char modification_date [ISODCL (831, 847)]; /* 8.4.26.1 */ 101 | char expiration_date [ISODCL (848, 864)]; /* 8.4.26.1 */ 102 | char effective_date [ISODCL (865, 881)]; /* 8.4.26.1 */ 103 | char file_structure_version [ISODCL (882, 882)]; /* 711 */ 104 | char unused4 [ISODCL (883, 883)]; 105 | char application_data [ISODCL (884, 1395)]; 106 | char unused5 [ISODCL (1396, 2048)]; 107 | }; 108 | 109 | 110 | #define HS_STANDARD_ID "CDROM" 111 | 112 | struct hs_volume_descriptor { 113 | char foo [ISODCL ( 1, 8)]; /* 733 */ 114 | char type [ISODCL ( 9, 9)]; /* 711 */ 115 | char id [ISODCL ( 10, 14)]; 116 | char version [ISODCL ( 15, 15)]; /* 711 */ 117 | char data[ISODCL(16,2048)]; 118 | }; 119 | 120 | 121 | struct hs_primary_descriptor { 122 | char foo [ISODCL ( 1, 8)]; /* 733 */ 123 | char type [ISODCL ( 9, 9)]; /* 711 */ 124 | char id [ISODCL ( 10, 14)]; 125 | char version [ISODCL ( 15, 15)]; /* 711 */ 126 | char unused1 [ISODCL ( 16, 16)]; /* 711 */ 127 | char system_id [ISODCL ( 17, 48)]; /* achars */ 128 | char volume_id [ISODCL ( 49, 80)]; /* dchars */ 129 | char unused2 [ISODCL ( 81, 88)]; /* 733 */ 130 | char volume_space_size [ISODCL ( 89, 96)]; /* 733 */ 131 | char unused3 [ISODCL ( 97, 128)]; /* 733 */ 132 | char volume_set_size [ISODCL (129, 132)]; /* 723 */ 133 | char volume_sequence_number [ISODCL (133, 136)]; /* 723 */ 134 | char logical_block_size [ISODCL (137, 140)]; /* 723 */ 135 | char path_table_size [ISODCL (141, 148)]; /* 733 */ 136 | char type_l_path_table [ISODCL (149, 152)]; /* 731 */ 137 | char unused4 [ISODCL (153, 180)]; /* 733 */ 138 | char root_directory_record [ISODCL (181, 214)]; /* 9.1 */ 139 | }; 140 | 141 | /* We use this to help us look up the parent inode numbers. */ 142 | 143 | struct iso_path_table{ 144 | unsigned char name_len[2]; /* 721 */ 145 | char extent[4]; /* 731 */ 146 | char parent[2]; /* 721 */ 147 | char name[0]; 148 | } __attribute__((packed)); 149 | 150 | /* high sierra is identical to iso, except that the date is only 6 bytes, and 151 | there is an extra reserved byte after the flags */ 152 | 153 | struct iso_directory_record { 154 | char length [ISODCL (1, 1)]; /* 711 */ 155 | char ext_attr_length [ISODCL (2, 2)]; /* 711 */ 156 | char extent [ISODCL (3, 10)]; /* 733 */ 157 | char size [ISODCL (11, 18)]; /* 733 */ 158 | char date [ISODCL (19, 25)]; /* 7 by 711 */ 159 | char flags [ISODCL (26, 26)]; 160 | char file_unit_size [ISODCL (27, 27)]; /* 711 */ 161 | char interleave [ISODCL (28, 28)]; /* 711 */ 162 | char volume_sequence_number [ISODCL (29, 32)]; /* 723 */ 163 | unsigned char name_len [ISODCL (33, 33)]; /* 711 */ 164 | char name [0]; 165 | } __attribute__((packed)); 166 | 167 | #define ISOFS_BLOCK_BITS 11 168 | #define ISOFS_BLOCK_SIZE 2048 169 | 170 | #define ISOFS_BUFFER_SIZE(INODE) ((INODE)->i_sb->s_blocksize) 171 | #define ISOFS_BUFFER_BITS(INODE) ((INODE)->i_sb->s_blocksize_bits) 172 | 173 | #define ISOFS_SUPER_MAGIC 0x9660 174 | 175 | #ifdef __KERNEL__ 176 | /* Number conversion inlines, named after the section in ISO 9660 177 | they correspond to. */ 178 | 179 | #include 180 | #include 181 | #include 182 | #include 183 | 184 | static inline struct isofs_sb_info *ISOFS_SB(struct super_block *sb) 185 | { 186 | return sb->s_fs_info; 187 | } 188 | 189 | static inline struct iso_inode_info *ISOFS_I(struct inode *inode) 190 | { 191 | return container_of(inode, struct iso_inode_info, vfs_inode); 192 | } 193 | 194 | static inline int isonum_711(char *p) 195 | { 196 | return *(u8 *)p; 197 | } 198 | static inline int isonum_712(char *p) 199 | { 200 | return *(s8 *)p; 201 | } 202 | static inline unsigned int isonum_721(char *p) 203 | { 204 | return le16_to_cpu(get_unaligned((__le16 *)p)); 205 | } 206 | static inline unsigned int isonum_722(char *p) 207 | { 208 | return be16_to_cpu(get_unaligned((__le16 *)p)); 209 | } 210 | static inline unsigned int isonum_723(char *p) 211 | { 212 | /* Ignore bigendian datum due to broken mastering programs */ 213 | return le16_to_cpu(get_unaligned((__le16 *)p)); 214 | } 215 | static inline unsigned int isonum_731(char *p) 216 | { 217 | return le32_to_cpu(get_unaligned((__le32 *)p)); 218 | } 219 | static inline unsigned int isonum_732(char *p) 220 | { 221 | return be32_to_cpu(get_unaligned((__le32 *)p)); 222 | } 223 | static inline unsigned int isonum_733(char *p) 224 | { 225 | /* Ignore bigendian datum due to broken mastering programs */ 226 | return le32_to_cpu(get_unaligned((__le32 *)p)); 227 | } 228 | extern int iso_date(char *, int); 229 | 230 | struct inode; /* To make gcc happy */ 231 | 232 | extern int parse_rock_ridge_inode(struct iso_directory_record *, struct inode *); 233 | extern int get_rock_ridge_filename(struct iso_directory_record *, char *, struct inode *); 234 | extern int isofs_name_translate(struct iso_directory_record *, char *, struct inode *); 235 | 236 | int get_joliet_filename(struct iso_directory_record *, unsigned char *, struct inode *); 237 | int get_acorn_filename(struct iso_directory_record *, char *, struct inode *); 238 | 239 | extern struct dentry *isofs_lookup(struct inode *, struct dentry *, struct nameidata *); 240 | extern struct buffer_head *isofs_bread(struct inode *, sector_t); 241 | extern int isofs_get_blocks(struct inode *, sector_t, struct buffer_head **, unsigned long); 242 | 243 | extern struct inode *isofs_iget(struct super_block *sb, 244 | unsigned long block, 245 | unsigned long offset); 246 | 247 | /* Because the inode number is no longer relevant to finding the 248 | * underlying meta-data for an inode, we are free to choose a more 249 | * convenient 32-bit number as the inode number. The inode numbering 250 | * scheme was recommended by Sergey Vlasov and Eric Lammerts. */ 251 | static inline unsigned long isofs_get_ino(unsigned long block, 252 | unsigned long offset, 253 | unsigned long bufbits) 254 | { 255 | return (block << (bufbits - 5)) | (offset >> 5); 256 | } 257 | 258 | /* Every directory can have many redundant directory entries scattered 259 | * throughout the directory tree. First there is the directory entry 260 | * with the name of the directory stored in the parent directory. 261 | * Then, there is the "." directory entry stored in the directory 262 | * itself. Finally, there are possibly many ".." directory entries 263 | * stored in all the subdirectories. 264 | * 265 | * In order for the NFS get_parent() method to work and for the 266 | * general consistency of the dcache, we need to make sure the 267 | * "i_iget5_block" and "i_iget5_offset" all point to exactly one of 268 | * the many redundant entries for each directory. We normalize the 269 | * block and offset by always making them point to the "." directory. 270 | * 271 | * Notice that we do not use the entry for the directory with the name 272 | * that is located in the parent directory. Even though choosing this 273 | * first directory is more natural, it is much easier to find the "." 274 | * entry in the NFS get_parent() method because it is implicitly 275 | * encoded in the "extent + ext_attr_length" fields of _all_ the 276 | * redundant entries for the directory. Thus, it can always be 277 | * reached regardless of which directory entry you have in hand. 278 | * 279 | * This works because the "." entry is simply the first directory 280 | * record when you start reading the file that holds all the directory 281 | * records, and this file starts at "extent + ext_attr_length" blocks. 282 | * Because the "." entry is always the first entry listed in the 283 | * directories file, the normalized "offset" value is always 0. 284 | * 285 | * You should pass the directory entry in "de". On return, "block" 286 | * and "offset" will hold normalized values. Only directories are 287 | * affected making it safe to call even for non-directory file 288 | * types. */ 289 | static inline void 290 | isofs_normalize_block_and_offset(struct iso_directory_record* de, 291 | unsigned long *block, 292 | unsigned long *offset) 293 | { 294 | /* Only directories are normalized. */ 295 | if (de->flags[0] & 2) { 296 | *offset = 0; 297 | *block = (unsigned long)isonum_733(de->extent) 298 | + (unsigned long)isonum_711(de->ext_attr_length); 299 | } 300 | } 301 | 302 | extern struct inode_operations isofs_dir_inode_operations; 303 | extern struct file_operations isofs_dir_operations; 304 | extern struct address_space_operations isofs_symlink_aops; 305 | extern struct export_operations isofs_export_ops; 306 | 307 | /* The following macros are used to check for memory leaks. */ 308 | #ifdef LEAK_CHECK 309 | #define free_s leak_check_free_s 310 | #define malloc leak_check_malloc 311 | #define sb_bread leak_check_bread 312 | #define brelse leak_check_brelse 313 | extern void * leak_check_malloc(unsigned int size); 314 | extern void leak_check_free_s(void * obj, int size); 315 | extern struct buffer_head * leak_check_bread(struct super_block *sb, int block); 316 | extern void leak_check_brelse(struct buffer_head * bh); 317 | #endif /* LEAK_CHECK */ 318 | 319 | #endif /* __KERNEL__ */ 320 | 321 | #endif 322 | -------------------------------------------------------------------------------- /runtime/md5.c: -------------------------------------------------------------------------------- 1 | /* md5.c - Functions to compute MD5 message digest of files or memory blocks 2 | according to the definition of MD5 in RFC 1321 from April 1992. 3 | Copyright (C) 1995 Software Foundation, Inc. 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2, or (at your option) 8 | any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ 18 | 19 | /* Written by Ulrich Drepper . */ 20 | 21 | #ifdef HAVE_CONFIG_H 22 | # include 23 | #endif 24 | 25 | #include 26 | 27 | # include 28 | # include 29 | 30 | #include "md5.h" 31 | 32 | #ifdef WORDS_BIGENDIAN 33 | # define SWAP(n) \ 34 | (((n) << 24) | (((n) & 0xff00) << 8) | (((n) >> 8) & 0xff00) | ((n) >> 24)) 35 | #else 36 | # define SWAP(n) (n) 37 | #endif 38 | 39 | 40 | /* This array contains the bytes used to pad the buffer to the next 41 | 64-byte boundary. (RFC 1321, 3.1: Step 1) */ 42 | static const unsigned char fillbuf[64] = { 0x80, 0 /* , 0, 0, ... */ }; 43 | 44 | 45 | /* Initialize structure containing state of computation. 46 | (RFC 1321, 3.3: Step 3) */ 47 | void 48 | md5_init_ctx (ctx) 49 | struct md5_ctx *ctx; 50 | { 51 | ctx->A = 0x67452301; 52 | ctx->B = 0xefcdab89; 53 | ctx->C = 0x98badcfe; 54 | ctx->D = 0x10325476; 55 | } 56 | 57 | /* Put result from CTX in first 16 bytes following RESBUF. The result must 58 | be in little endian byte order. */ 59 | void * 60 | md5_read_ctx (ctx, resbuf) 61 | const struct md5_ctx *ctx; 62 | void *resbuf; 63 | { 64 | ((md5_uint32 *) resbuf)[0] = SWAP (ctx->A); 65 | ((md5_uint32 *) resbuf)[1] = SWAP (ctx->B); 66 | ((md5_uint32 *) resbuf)[2] = SWAP (ctx->C); 67 | ((md5_uint32 *) resbuf)[3] = SWAP (ctx->D); 68 | 69 | return resbuf; 70 | } 71 | 72 | /* Compute MD5 message digest for bytes read from STREAM. The 73 | resulting message digest number will be written into the 16 bytes 74 | beginning at RESBLOCK. */ 75 | int 76 | md5_stream (stream, resblock) 77 | FILE *stream; 78 | void *resblock; 79 | { 80 | /* Important: BLOCKSIZE must be a multiple of 64. */ 81 | #define BLOCKSIZE 4096 82 | struct md5_ctx ctx; 83 | md5_uint32 len[2]; 84 | char buffer[BLOCKSIZE + 72]; 85 | size_t pad, sum; 86 | 87 | /* Initialize the computation context. */ 88 | md5_init_ctx (&ctx); 89 | 90 | len[0] = 0; 91 | len[1] = 0; 92 | 93 | /* Iterate over full file contents. */ 94 | while (1) 95 | { 96 | /* We read the file in blocks of BLOCKSIZE bytes. One call of the 97 | computation function processes the whole buffer so that with the 98 | next round of the loop another block can be read. */ 99 | size_t n; 100 | sum = 0; 101 | 102 | /* Read block. Take care for partial reads. */ 103 | do 104 | { 105 | n = fread (buffer, 1, BLOCKSIZE - sum, stream); 106 | 107 | sum += n; 108 | } 109 | while (sum < BLOCKSIZE && n != 0); 110 | if (n == 0 && ferror (stream)) 111 | return 1; 112 | 113 | /* RFC 1321 specifies the possible length of the file up to 2^64 bits. 114 | Here we only compute the number of bytes. Do a double word 115 | increment. */ 116 | len[0] += sum; 117 | if (len[0] < sum) 118 | ++len[1]; 119 | 120 | /* If end of file is reached, end the loop. */ 121 | if (n == 0) 122 | break; 123 | 124 | /* Process buffer with BLOCKSIZE bytes. Note that 125 | BLOCKSIZE % 64 == 0 126 | */ 127 | md5_process_block (buffer, BLOCKSIZE, &ctx); 128 | } 129 | 130 | /* We can copy 64 byte because the buffer is always big enough. FILLBUF 131 | contains the needed bits. */ 132 | memcpy (&buffer[sum], fillbuf, 64); 133 | 134 | /* Compute amount of padding bytes needed. Alignment is done to 135 | (N + PAD) % 64 == 56 136 | There is always at least one byte padded. I.e. even the alignment 137 | is correctly aligned 64 padding bytes are added. */ 138 | pad = sum & 63; 139 | pad = pad >= 56 ? 64 + 56 - pad : 56 - pad; 140 | 141 | /* Put the 64-bit file length in *bits* at the end of the buffer. */ 142 | *(md5_uint32 *) &buffer[sum + pad] = SWAP (len[0] << 3); 143 | *(md5_uint32 *) &buffer[sum + pad + 4] = SWAP ((len[1] << 3) 144 | | (len[0] >> 29)); 145 | 146 | /* Process last bytes. */ 147 | md5_process_block (buffer, sum + pad + 8, &ctx); 148 | 149 | /* Construct result in desired memory. */ 150 | md5_read_ctx (&ctx, resblock); 151 | return 0; 152 | } 153 | 154 | /* Compute MD5 message digest for LEN bytes beginning at BUFFER. The 155 | result is always in little endian byte order, so that a byte-wise 156 | output yields to the wanted ASCII representation of the message 157 | digest. */ 158 | void * 159 | md5_buffer (buffer, len, resblock) 160 | const char *buffer; 161 | size_t len; 162 | void *resblock; 163 | { 164 | struct md5_ctx ctx; 165 | char restbuf[64 + 72]; 166 | size_t blocks = len & ~63; 167 | size_t pad, rest; 168 | 169 | /* Initialize the computation context. */ 170 | md5_init_ctx (&ctx); 171 | 172 | /* Process whole buffer but last len % 64 bytes. */ 173 | md5_process_block (buffer, blocks, &ctx); 174 | 175 | /* REST bytes are not processed yet. */ 176 | rest = len - blocks; 177 | /* Copy to own buffer. */ 178 | memcpy (restbuf, &buffer[blocks], rest); 179 | /* Append needed fill bytes at end of buffer. We can copy 64 byte 180 | because the buffer is always big enough. */ 181 | memcpy (&restbuf[rest], fillbuf, 64); 182 | 183 | /* PAD bytes are used for padding to correct alignment. Note that 184 | always at least one byte is padded. */ 185 | pad = rest >= 56 ? 64 + 56 - rest : 56 - rest; 186 | 187 | /* Put length of buffer in *bits* in last eight bytes. */ 188 | *(md5_uint32 *) &restbuf[rest + pad] = (md5_uint32) SWAP (len << 3); 189 | *(md5_uint32 *) &restbuf[rest + pad + 4] = (md5_uint32) SWAP (len >> 29); 190 | 191 | /* Process last bytes. */ 192 | md5_process_block (restbuf, rest + pad + 8, &ctx); 193 | 194 | /* Put result in desired memory area. */ 195 | return md5_read_ctx (&ctx, resblock); 196 | } 197 | 198 | 199 | /* These are the four functions used in the four steps of the MD5 algorithm 200 | and defined in the RFC 1321. The first function is a little bit optimized 201 | (as found in Colin Plumbs public domain implementation). */ 202 | /* #define FF(b, c, d) ((b & c) | (~b & d)) */ 203 | #define FF(b, c, d) (d ^ (b & (c ^ d))) 204 | #define FG(b, c, d) FF (d, b, c) 205 | #define FH(b, c, d) (b ^ c ^ d) 206 | #define FI(b, c, d) (c ^ (b | ~d)) 207 | 208 | /* Process LEN bytes of BUFFER, accumulating context into CTX. 209 | It is assumed that LEN % 64 == 0. */ 210 | 211 | void 212 | md5_process_block (buffer, len, ctx) 213 | const void *buffer; 214 | size_t len; 215 | struct md5_ctx *ctx; 216 | { 217 | md5_uint32 correct_words[16]; 218 | const md5_uint32 *words = buffer; 219 | size_t nwords = len / sizeof (md5_uint32); 220 | const md5_uint32 *endp = words + nwords; 221 | md5_uint32 A = ctx->A; 222 | md5_uint32 B = ctx->B; 223 | md5_uint32 C = ctx->C; 224 | md5_uint32 D = ctx->D; 225 | 226 | /* Process all bytes in the buffer with 64 bytes in each round of 227 | the loop. */ 228 | while (words < endp) 229 | { 230 | md5_uint32 *cwp = correct_words; 231 | md5_uint32 A_save = A; 232 | md5_uint32 B_save = B; 233 | md5_uint32 C_save = C; 234 | md5_uint32 D_save = D; 235 | 236 | /* First round: using the given function, the context and a constant 237 | the next context is computed. Because the algorithms processing 238 | unit is a 32-bit word and it is determined to work on words in 239 | little endian byte order we perhaps have to change the byte order 240 | before the computation. To reduce the work for the next steps 241 | we store the swapped words in the array CORRECT_WORDS. */ 242 | 243 | #define OP(a, b, c, d, s, T) \ 244 | do \ 245 | { \ 246 | a += FF (b, c, d) + (*cwp++ = SWAP (*words)) + T; \ 247 | ++words; \ 248 | CYCLIC (a, s); \ 249 | a += b; \ 250 | } \ 251 | while (0) 252 | 253 | /* It is unfortunate that C does not provide an operator for 254 | cyclic rotation. Hope the C compiler is smart enough. */ 255 | #define CYCLIC(w, s) (w = (w << s) | (w >> (32 - s))) 256 | 257 | /* Before we start, one word to the strange constants. 258 | They are defined in RFC 1321 as 259 | 260 | T[i] = (int) (4294967296.0 * fabs (sin (i))), i=1..64 261 | */ 262 | 263 | /* Round 1. */ 264 | OP (A, B, C, D, 7, 0xd76aa478); 265 | OP (D, A, B, C, 12, 0xe8c7b756); 266 | OP (C, D, A, B, 17, 0x242070db); 267 | OP (B, C, D, A, 22, 0xc1bdceee); 268 | OP (A, B, C, D, 7, 0xf57c0faf); 269 | OP (D, A, B, C, 12, 0x4787c62a); 270 | OP (C, D, A, B, 17, 0xa8304613); 271 | OP (B, C, D, A, 22, 0xfd469501); 272 | OP (A, B, C, D, 7, 0x698098d8); 273 | OP (D, A, B, C, 12, 0x8b44f7af); 274 | OP (C, D, A, B, 17, 0xffff5bb1); 275 | OP (B, C, D, A, 22, 0x895cd7be); 276 | OP (A, B, C, D, 7, 0x6b901122); 277 | OP (D, A, B, C, 12, 0xfd987193); 278 | OP (C, D, A, B, 17, 0xa679438e); 279 | OP (B, C, D, A, 22, 0x49b40821); 280 | 281 | /* For the second to fourth round we have the possibly swapped words 282 | in CORRECT_WORDS. Redefine the macro to take an additional first 283 | argument specifying the function to use. */ 284 | #undef OP 285 | #define OP(f, a, b, c, d, k, s, T) \ 286 | do \ 287 | { \ 288 | a += f (b, c, d) + correct_words[k] + T; \ 289 | CYCLIC (a, s); \ 290 | a += b; \ 291 | } \ 292 | while (0) 293 | 294 | /* Round 2. */ 295 | OP (FG, A, B, C, D, 1, 5, 0xf61e2562); 296 | OP (FG, D, A, B, C, 6, 9, 0xc040b340); 297 | OP (FG, C, D, A, B, 11, 14, 0x265e5a51); 298 | OP (FG, B, C, D, A, 0, 20, 0xe9b6c7aa); 299 | OP (FG, A, B, C, D, 5, 5, 0xd62f105d); 300 | OP (FG, D, A, B, C, 10, 9, 0x02441453); 301 | OP (FG, C, D, A, B, 15, 14, 0xd8a1e681); 302 | OP (FG, B, C, D, A, 4, 20, 0xe7d3fbc8); 303 | OP (FG, A, B, C, D, 9, 5, 0x21e1cde6); 304 | OP (FG, D, A, B, C, 14, 9, 0xc33707d6); 305 | OP (FG, C, D, A, B, 3, 14, 0xf4d50d87); 306 | OP (FG, B, C, D, A, 8, 20, 0x455a14ed); 307 | OP (FG, A, B, C, D, 13, 5, 0xa9e3e905); 308 | OP (FG, D, A, B, C, 2, 9, 0xfcefa3f8); 309 | OP (FG, C, D, A, B, 7, 14, 0x676f02d9); 310 | OP (FG, B, C, D, A, 12, 20, 0x8d2a4c8a); 311 | 312 | /* Round 3. */ 313 | OP (FH, A, B, C, D, 5, 4, 0xfffa3942); 314 | OP (FH, D, A, B, C, 8, 11, 0x8771f681); 315 | OP (FH, C, D, A, B, 11, 16, 0x6d9d6122); 316 | OP (FH, B, C, D, A, 14, 23, 0xfde5380c); 317 | OP (FH, A, B, C, D, 1, 4, 0xa4beea44); 318 | OP (FH, D, A, B, C, 4, 11, 0x4bdecfa9); 319 | OP (FH, C, D, A, B, 7, 16, 0xf6bb4b60); 320 | OP (FH, B, C, D, A, 10, 23, 0xbebfbc70); 321 | OP (FH, A, B, C, D, 13, 4, 0x289b7ec6); 322 | OP (FH, D, A, B, C, 0, 11, 0xeaa127fa); 323 | OP (FH, C, D, A, B, 3, 16, 0xd4ef3085); 324 | OP (FH, B, C, D, A, 6, 23, 0x04881d05); 325 | OP (FH, A, B, C, D, 9, 4, 0xd9d4d039); 326 | OP (FH, D, A, B, C, 12, 11, 0xe6db99e5); 327 | OP (FH, C, D, A, B, 15, 16, 0x1fa27cf8); 328 | OP (FH, B, C, D, A, 2, 23, 0xc4ac5665); 329 | 330 | /* Round 4. */ 331 | OP (FI, A, B, C, D, 0, 6, 0xf4292244); 332 | OP (FI, D, A, B, C, 7, 10, 0x432aff97); 333 | OP (FI, C, D, A, B, 14, 15, 0xab9423a7); 334 | OP (FI, B, C, D, A, 5, 21, 0xfc93a039); 335 | OP (FI, A, B, C, D, 12, 6, 0x655b59c3); 336 | OP (FI, D, A, B, C, 3, 10, 0x8f0ccc92); 337 | OP (FI, C, D, A, B, 10, 15, 0xffeff47d); 338 | OP (FI, B, C, D, A, 1, 21, 0x85845dd1); 339 | OP (FI, A, B, C, D, 8, 6, 0x6fa87e4f); 340 | OP (FI, D, A, B, C, 15, 10, 0xfe2ce6e0); 341 | OP (FI, C, D, A, B, 6, 15, 0xa3014314); 342 | OP (FI, B, C, D, A, 13, 21, 0x4e0811a1); 343 | OP (FI, A, B, C, D, 4, 6, 0xf7537e82); 344 | OP (FI, D, A, B, C, 11, 10, 0xbd3af235); 345 | OP (FI, C, D, A, B, 2, 15, 0x2ad7d2bb); 346 | OP (FI, B, C, D, A, 9, 21, 0xeb86d391); 347 | 348 | /* Add the starting values of the context. */ 349 | A += A_save; 350 | B += B_save; 351 | C += C_save; 352 | D += D_save; 353 | } 354 | 355 | /* Put checksum in context given as argument. */ 356 | ctx->A = A; 357 | ctx->B = B; 358 | ctx->C = C; 359 | ctx->D = D; 360 | } -------------------------------------------------------------------------------- /core/xdg/IniFile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base Class for DesktopEntry, IconTheme and IconData 3 | """ 4 | 5 | import sys 6 | import re, os.path, codecs 7 | import xdg.Locale 8 | import gettext 9 | from . import Exceptions 10 | 11 | class IniFile: 12 | defaultGroup = '' 13 | fileExtension = '' 14 | 15 | filename = '' 16 | gettext_domain = None 17 | 18 | tainted = False 19 | 20 | def __init__(self, filename=None): 21 | self.content = dict() 22 | if filename: 23 | self.parse(filename) 24 | 25 | def __cmp__(self, other): 26 | return cmp(self.content, other.content) 27 | 28 | def parse(self, filename, headers): 29 | # for performance reasons 30 | content = self.content 31 | 32 | if not os.path.isfile(filename): 33 | raise ParsingError("File not found", filename) 34 | 35 | try: 36 | fd = open(filename, 'r') 37 | except IOError as e: 38 | if Exceptions.debug: 39 | raise e 40 | else: 41 | return 42 | 43 | # parse file 44 | for line in fd: 45 | line = line.strip() 46 | # empty line 47 | if not line: 48 | continue 49 | # comment 50 | elif line[0] == '#': 51 | continue 52 | # new group 53 | elif line[0] == '[': 54 | currentGroup = line.lstrip("[").rstrip("]") 55 | if Exceptions.debug and self.hasGroup(currentGroup): 56 | raise Exceptions.DuplicateGroupError(currentGroup, filename) 57 | else: 58 | content[currentGroup] = {} 59 | # key 60 | else: 61 | index = line.find("=") 62 | key = line[0:index].strip() 63 | value = line[index+1:].strip() 64 | try: 65 | if Exceptions.debug and self.hasKey(key, currentGroup): 66 | raise Exceptions.DuplicateKeyError(key, currentGroup, filename) 67 | else: 68 | content[currentGroup][key] = value 69 | except (IndexError, UnboundLocalError): 70 | raise ParsingError("[%s]-Header missing" % headers[0], filename) 71 | 72 | fd.close() 73 | 74 | self.filename = filename 75 | self.tainted = False 76 | 77 | # check header 78 | for header in headers: 79 | if header in content: 80 | self.defaultGroup = header 81 | break 82 | else: 83 | raise ParsingError("[%s]-Header missing" % headers[0], filename) 84 | 85 | # check for gettext domain 86 | e = self.content.get('Desktop Entry', {}) 87 | self.gettext_domain = e.get('X-GNOME-Gettext-Domain', 88 | e.get('X-Ubuntu-Gettext-Domain', None)) 89 | 90 | # start stuff to access the keys 91 | def get(self, key, group=None, locale=False, type="string", list=False): 92 | # set default group 93 | if not group: 94 | group = self.defaultGroup 95 | 96 | # return key (with locale) 97 | if group in self.content and key in self.content[group]: 98 | if locale: 99 | key = self.__addLocale(key, group) 100 | if key.endswith(']') or not self.gettext_domain: 101 | # inline translations 102 | value = self.content[group][key] 103 | else: 104 | value = gettext.dgettext(self.gettext_domain, self.content[group][key]) 105 | else: 106 | value = self.content[group][key] 107 | else: 108 | if Exceptions.debug: 109 | if not groupt in self.content: 110 | raise Exceptions.NoGroupError(group, self.filename) 111 | elif key not in self.content[group]: 112 | raise Exceptions.NoKeyError(key, group, self.filename) 113 | else: 114 | value = "" 115 | 116 | if list == True: 117 | values = self.getList(value) 118 | result = [] 119 | else: 120 | values = [value] 121 | 122 | for value in values: 123 | if type == "string" and locale == True: 124 | if sys.version_info[0] < 3: 125 | value = value.decode("utf-8", "ignore") 126 | elif type == "boolean": 127 | value = self.__getBoolean(value) 128 | elif type == "integer": 129 | try: 130 | value = int(value) 131 | except ValueError: 132 | value = 0 133 | elif type == "numeric": 134 | try: 135 | value = float(value) 136 | except ValueError: 137 | value = 0.0 138 | elif type == "regex": 139 | value = re.compile(value) 140 | elif type == "point": 141 | value = value.split(",") 142 | 143 | if list == True: 144 | result.append(value) 145 | else: 146 | result = value 147 | 148 | return result 149 | # end stuff to access the keys 150 | 151 | # start subget 152 | def getList(self, string): 153 | if re.search(r"(? 0: 330 | key = key + "[" + xdg.Locale.langs[0] + "]" 331 | 332 | try: 333 | if isinstance(value, unicode): 334 | self.content[group][key] = value.encode("utf-8", "ignore") 335 | else: 336 | self.content[group][key] = value 337 | except KeyError: 338 | raise NoGroupError(group, self.filename) 339 | 340 | self.tainted = (value == self.get(key, group)) 341 | 342 | def addGroup(self, group): 343 | if self.hasGroup(group): 344 | if Exceptions.debug: 345 | raise Exceptions.DuplicateGroupError(group, self.filename) 346 | else: 347 | pass 348 | else: 349 | self.content[group] = {} 350 | self.tainted = True 351 | 352 | def removeGroup(self, group): 353 | existed = group in self.content 354 | if existed: 355 | del self.content[group] 356 | self.tainted = True 357 | else: 358 | if debug: 359 | raise Exceptions.NoGroupError(group, self.filename) 360 | return existed 361 | 362 | def removeKey(self, key, group=None, locales=True): 363 | # set default group 364 | if not group: 365 | group = self.defaultGroup 366 | 367 | try: 368 | if locales: 369 | for (name, value) in self.content[group].items(): 370 | if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: 371 | value = self.content[group][name] 372 | del self.content[group][name] 373 | value = self.content[group][key] 374 | del self.content[group][key] 375 | self.tainted = True 376 | return value 377 | except KeyError as e: 378 | if debug: 379 | if e == group: 380 | raise Exceptions.NoGroupError(group, self.filename) 381 | else: 382 | raise Exceptions.NoKeyError(key, group, self.filename) 383 | else: 384 | return "" 385 | 386 | # misc 387 | def groups(self): 388 | return self.content.keys() 389 | 390 | def hasGroup(self, group): 391 | if group in self.content: 392 | return True 393 | else: 394 | return False 395 | 396 | def hasKey(self, key, group=None): 397 | # set default group 398 | if not group: 399 | group = self.defaultGroup 400 | 401 | if key in self.content[group]: 402 | return True 403 | else: 404 | return False 405 | 406 | def getFileName(self): 407 | return self.filename 408 | -------------------------------------------------------------------------------- /core/xdg/IconTheme.py: -------------------------------------------------------------------------------- 1 | """ 2 | Complete implementation of the XDG Icon Spec Version 0.8 3 | http://standards.freedesktop.org/icon-theme-spec/ 4 | """ 5 | 6 | import os, sys, time 7 | 8 | from xdg.IniFile import * 9 | from xdg.BaseDirectory import * 10 | from xdg.Exceptions import * 11 | 12 | import xdg.Config 13 | 14 | class IconTheme(IniFile): 15 | "Class to parse and validate IconThemes" 16 | def __init__(self): 17 | IniFile.__init__(self) 18 | 19 | def __repr__(self): 20 | return self.name 21 | 22 | def parse(self, file): 23 | IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) 24 | self.dir = os.path.dirname(file) 25 | (nil, self.name) = os.path.split(self.dir) 26 | 27 | def getDir(self): 28 | return self.dir 29 | 30 | # Standard Keys 31 | def getName(self): 32 | return self.get('Name', locale=True) 33 | def getComment(self): 34 | return self.get('Comment', locale=True) 35 | def getInherits(self): 36 | return self.get('Inherits', list=True) 37 | def getDirectories(self): 38 | return self.get('Directories', list=True) 39 | def getHidden(self): 40 | return self.get('Hidden', type="boolean") 41 | def getExample(self): 42 | return self.get('Example') 43 | 44 | # Per Directory Keys 45 | def getSize(self, directory): 46 | return self.get('Size', type="integer", group=directory) 47 | def getContext(self, directory): 48 | return self.get('Context', group=directory) 49 | def getType(self, directory): 50 | value = self.get('Type', group=directory) 51 | if value: 52 | return value 53 | else: 54 | return "Threshold" 55 | def getMaxSize(self, directory): 56 | value = self.get('MaxSize', type="integer", group=directory) 57 | if value or value == 0: 58 | return value 59 | else: 60 | return self.getSize(directory) 61 | def getMinSize(self, directory): 62 | value = self.get('MinSize', type="integer", group=directory) 63 | if value or value == 0: 64 | return value 65 | else: 66 | return self.getSize(directory) 67 | def getThreshold(self, directory): 68 | value = self.get('Threshold', type="integer", group=directory) 69 | if value or value == 0: 70 | return value 71 | else: 72 | return 2 73 | 74 | # validation stuff 75 | def checkExtras(self): 76 | # header 77 | if self.defaultGroup == "KDE Icon Theme": 78 | self.warnings.append('[KDE Icon Theme]-Header is deprecated') 79 | 80 | # file extension 81 | if self.fileExtension == ".theme": 82 | pass 83 | elif self.fileExtension == ".desktop": 84 | self.warnings.append('.desktop fileExtension is deprecated') 85 | else: 86 | self.warnings.append('Unknown File extension') 87 | 88 | # Check required keys 89 | # Name 90 | try: 91 | self.name = self.content[self.defaultGroup]["Name"] 92 | except KeyError: 93 | self.errors.append("Key 'Name' is missing") 94 | 95 | # Comment 96 | try: 97 | self.comment = self.content[self.defaultGroup]["Comment"] 98 | except KeyError: 99 | self.errors.append("Key 'Comment' is missing") 100 | 101 | # Directories 102 | try: 103 | self.directories = self.content[self.defaultGroup]["Directories"] 104 | except KeyError: 105 | self.errors.append("Key 'Directories' is missing") 106 | 107 | def checkGroup(self, group): 108 | # check if group header is valid 109 | if group == self.defaultGroup: 110 | pass 111 | elif group in self.getDirectories(): 112 | try: 113 | self.type = self.content[group]["Type"] 114 | except KeyError: 115 | self.type = "Threshold" 116 | try: 117 | self.name = self.content[group]["Name"] 118 | except KeyError: 119 | self.errors.append("Key 'Name' in Group '%s' is missing" % group) 120 | elif not (re.match("^\[X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group): 121 | self.errors.append("Invalid Group name: %s" % group) 122 | 123 | def checkKey(self, key, value, group): 124 | # standard keys 125 | if group == self.defaultGroup: 126 | if re.match("^Name"+xdg.Locale.regex+"$", key): 127 | pass 128 | elif re.match("^Comment"+xdg.Locale.regex+"$", key): 129 | pass 130 | elif key == "Inherits": 131 | self.checkValue(key, value, list=True) 132 | elif key == "Directories": 133 | self.checkValue(key, value, list=True) 134 | elif key == "Hidden": 135 | self.checkValue(key, value, type="boolean") 136 | elif key == "Example": 137 | self.checkValue(key, value) 138 | elif re.match("^X-[a-zA-Z0-9-]+", key): 139 | pass 140 | else: 141 | self.errors.append("Invalid key: %s" % key) 142 | elif group in self.getDirectories(): 143 | if key == "Size": 144 | self.checkValue(key, value, type="integer") 145 | elif key == "Context": 146 | self.checkValue(key, value) 147 | elif key == "Type": 148 | self.checkValue(key, value) 149 | if value not in ["Fixed", "Scalable", "Threshold"]: 150 | self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) 151 | elif key == "MaxSize": 152 | self.checkValue(key, value, type="integer") 153 | if self.type != "Scalable": 154 | self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) 155 | elif key == "MinSize": 156 | self.checkValue(key, value, type="integer") 157 | if self.type != "Scalable": 158 | self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) 159 | elif key == "Threshold": 160 | self.checkValue(key, value, type="integer") 161 | if self.type != "Threshold": 162 | self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) 163 | elif re.match("^X-[a-zA-Z0-9-]+", key): 164 | pass 165 | else: 166 | self.errors.append("Invalid key: %s" % key) 167 | 168 | 169 | class IconData(IniFile): 170 | "Class to parse and validate IconData Files" 171 | def __init__(self): 172 | IniFile.__init__(self) 173 | 174 | def __repr__(self): 175 | return self.getDisplayName() 176 | 177 | def parse(self, file): 178 | IniFile.parse(self, file, ["Icon Data"]) 179 | 180 | # Standard Keys 181 | def getDisplayName(self): 182 | return self.get('DisplayName', locale=True) 183 | def getEmbeddedTextRectangle(self): 184 | return self.get('EmbeddedTextRectangle', list=True) 185 | def getAttachPoints(self): 186 | return self.get('AttachPoints', type="point", list=True) 187 | 188 | # validation stuff 189 | def checkExtras(self): 190 | # file extension 191 | if self.fileExtension != ".icon": 192 | self.warnings.append('Unknown File extension') 193 | 194 | def checkGroup(self, group): 195 | # check if group header is valid 196 | if not (group == self.defaultGroup \ 197 | or (re.match("^\[X-", group) and group.encode("ascii", 'ignore') == group)): 198 | self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) 199 | 200 | def checkKey(self, key, value, group): 201 | # standard keys 202 | if re.match("^DisplayName"+xdg.Locale.regex+"$", key): 203 | pass 204 | elif key == "EmbeddedTextRectangle": 205 | self.checkValue(key, value, type="integer", list=True) 206 | elif key == "AttachPoints": 207 | self.checkValue(key, value, type="point", list=True) 208 | elif re.match("^X-[a-zA-Z0-9-]+", key): 209 | pass 210 | else: 211 | self.errors.append("Invalid key: %s" % key) 212 | 213 | 214 | 215 | icondirs = [] 216 | for basedir in xdg_data_dirs: 217 | icondirs.append(os.path.join(basedir, "icons")) 218 | icondirs.append(os.path.join(basedir, "pixmaps")) 219 | icondirs.append(os.path.expanduser("~/.icons")) 220 | 221 | # just cache variables, they give a 10x speed improvement 222 | themes = [] 223 | cache = dict() 224 | dache = dict() 225 | eache = dict() 226 | 227 | def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): 228 | global themes 229 | 230 | if size == None: 231 | size = xdg.Config.icon_size 232 | if theme == None: 233 | theme = xdg.Config.icon_theme 234 | 235 | # if we have an absolute path, just return it 236 | if os.path.isabs(iconname): 237 | return iconname 238 | 239 | # check if it has an extension and strip it 240 | if os.path.splitext(iconname)[1][1:] in extensions: 241 | iconname = os.path.splitext(iconname)[0] 242 | 243 | # parse theme files 244 | try: 245 | if themes[0].name != theme: 246 | themes = [] 247 | __addTheme(theme) 248 | except IndexError: 249 | __addTheme(theme) 250 | 251 | # more caching (icon looked up in the last 5 seconds?) 252 | tmp = "".join([iconname, str(size), theme, "".join(extensions)]) 253 | if tmp in eache: 254 | if int(time.time() - eache[tmp][0]) >= xdg.Config.cache_time: 255 | del eache[tmp] 256 | else: 257 | return eache[tmp][1] 258 | 259 | for thme in themes: 260 | icon = LookupIcon(iconname, size, thme, extensions) 261 | if icon: 262 | eache[tmp] = [time.time(), icon] 263 | return icon 264 | 265 | # cache stuff again (directories lookuped up in the last 5 seconds?) 266 | for directory in icondirs: 267 | if (directory not in dache \ 268 | or (int(time.time() - dache[directory][1]) >= xdg.Config.cache_time \ 269 | and dache[directory][2] < os.path.getmtime(directory))) \ 270 | and os.path.isdir(directory): 271 | dache[directory] = [os.listdir(directory), time.time(), os.path.getmtime(directory)] 272 | 273 | for dir, values in dache.items(): 274 | for extension in extensions: 275 | try: 276 | if iconname + "." + extension in values[0]: 277 | icon = os.path.join(dir, iconname + "." + extension) 278 | eache[tmp] = [time.time(), icon] 279 | return icon 280 | except UnicodeDecodeError as e: 281 | if debug: 282 | raise e 283 | else: 284 | pass 285 | 286 | # we haven't found anything? "hicolor" is our fallback 287 | if theme != "hicolor": 288 | icon = getIconPath(iconname, size, "hicolor") 289 | eache[tmp] = [time.time(), icon] 290 | return icon 291 | 292 | def getIconData(path): 293 | if os.path.isfile(path): 294 | dirname = os.path.dirname(path) 295 | basename = os.path.basename(path) 296 | if os.path.isfile(os.path.join(dirname, basename + ".icon")): 297 | data = IconData() 298 | data.parse(os.path.join(dirname, basename + ".icon")) 299 | return data 300 | 301 | def __addTheme(theme): 302 | for dir in icondirs: 303 | if os.path.isfile(os.path.join(dir, theme, "index.theme")): 304 | __parseTheme(os.path.join(dir,theme, "index.theme")) 305 | break 306 | elif os.path.isfile(os.path.join(dir, theme, "index.desktop")): 307 | __parseTheme(os.path.join(dir,theme, "index.desktop")) 308 | break 309 | else: 310 | if debug: 311 | raise NoThemeError(theme) 312 | 313 | def __parseTheme(file): 314 | theme = IconTheme() 315 | theme.parse(file) 316 | themes.append(theme) 317 | for subtheme in theme.getInherits(): 318 | __addTheme(subtheme) 319 | 320 | def LookupIcon(iconname, size, theme, extensions): 321 | # look for the cache 322 | if theme.name not in cache: 323 | cache[theme.name] = [] 324 | cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup 325 | cache[theme.name].append(0) # [1] mtime 326 | cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] 327 | 328 | # cache stuff (directory lookuped up the in the last 5 seconds?) 329 | if int(time.time() - cache[theme.name][0]) >= xdg.Config.cache_time: 330 | cache[theme.name][0] = time.time() 331 | for subdir in theme.getDirectories(): 332 | for directory in icondirs: 333 | dir = os.path.join(directory,theme.name,subdir) 334 | if (dir not in cache[theme.name][2] \ 335 | or cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ 336 | and subdir != "" \ 337 | and os.path.isdir(dir): 338 | cache[theme.name][2][dir] = [subdir, os.listdir(dir)] 339 | cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) 340 | 341 | for dir, values in cache[theme.name][2].items(): 342 | if DirectoryMatchesSize(values[0], size, theme): 343 | for extension in extensions: 344 | if iconname + "." + extension in values[1]: 345 | return os.path.join(dir, iconname + "." + extension) 346 | 347 | minimal_size = float("inf") 348 | closest_filename = "" 349 | for dir, values in cache[theme.name][2].items(): 350 | distance = DirectorySizeDistance(values[0], size, theme) 351 | if distance < minimal_size: 352 | for extension in extensions: 353 | if iconname + "." + extension in values[1]: 354 | closest_filename = os.path.join(dir, iconname + "." + extension) 355 | minimal_size = distance 356 | 357 | return closest_filename 358 | 359 | def DirectoryMatchesSize(subdir, iconsize, theme): 360 | Type = theme.getType(subdir) 361 | Size = theme.getSize(subdir) 362 | Threshold = theme.getThreshold(subdir) 363 | MinSize = theme.getMinSize(subdir) 364 | MaxSize = theme.getMaxSize(subdir) 365 | if Type == "Fixed": 366 | return Size == iconsize 367 | elif Type == "Scaleable": 368 | return MinSize <= iconsize <= MaxSize 369 | elif Type == "Threshold": 370 | return Size - Threshold <= iconsize <= Size + Threshold 371 | 372 | def DirectorySizeDistance(subdir, iconsize, theme): 373 | Type = theme.getType(subdir) 374 | Size = theme.getSize(subdir) 375 | Threshold = theme.getThreshold(subdir) 376 | MinSize = theme.getMinSize(subdir) 377 | MaxSize = theme.getMaxSize(subdir) 378 | if Type == "Fixed": 379 | return abs(Size - iconsize) 380 | elif Type == "Scalable": 381 | if iconsize < MinSize: 382 | return MinSize - iconsize 383 | elif iconsize > MaxSize: 384 | return MaxSize - iconsize 385 | return 0 386 | elif Type == "Threshold": 387 | if iconsize < Size - Threshold: 388 | return MinSize - iconsize 389 | elif iconsize > Size + Threshold: 390 | return iconsize - MaxSize 391 | return 0 392 | -------------------------------------------------------------------------------- /core/xdg/Mime.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is based on a rox module (LGPL): 3 | 4 | http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log 5 | 6 | This module provides access to the shared MIME database. 7 | 8 | types is a dictionary of all known MIME types, indexed by the type name, e.g. 9 | types['application/x-python'] 10 | 11 | Applications can install information about MIME types by storing an 12 | XML file as /packages/.xml and running the 13 | update-mime-database command, which is provided by the freedesktop.org 14 | shared mime database package. 15 | 16 | See http://www.freedesktop.org/standards/shared-mime-info-spec/ for 17 | information about the format of these files. 18 | 19 | (based on version 0.13) 20 | """ 21 | 22 | import os 23 | import stat 24 | import fnmatch 25 | 26 | import xdg.BaseDirectory 27 | import xdg.Locale 28 | 29 | from xml.dom import Node, minidom, XML_NAMESPACE 30 | 31 | FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' 32 | 33 | types = {} # Maps MIME names to type objects 34 | 35 | exts = None # Maps extensions to types 36 | globs = None # List of (glob, type) pairs 37 | literals = None # Maps liternal names to types 38 | magic = None 39 | 40 | def _get_node_data(node): 41 | """Get text of XML node""" 42 | return ''.join([n.nodeValue for n in node.childNodes]).strip() 43 | 44 | def lookup(media, subtype = None): 45 | "Get the MIMEtype object for this type, creating a new one if needed." 46 | if subtype is None and '/' in media: 47 | media, subtype = media.split('/', 1) 48 | if (media, subtype) not in types: 49 | types[(media, subtype)] = MIMEtype(media, subtype) 50 | return types[(media, subtype)] 51 | 52 | class MIMEtype: 53 | """Type holding data about a MIME type""" 54 | def __init__(self, media, subtype): 55 | "Don't use this constructor directly; use mime.lookup() instead." 56 | assert media and '/' not in media 57 | assert subtype and '/' not in subtype 58 | assert (media, subtype) not in types 59 | 60 | self.media = media 61 | self.subtype = subtype 62 | self._comment = None 63 | 64 | def _load(self): 65 | "Loads comment for current language. Use get_comment() instead." 66 | resource = os.path.join('mime', self.media, self.subtype + '.xml') 67 | for path in xdg.BaseDirectory.load_data_paths(resource): 68 | doc = minidom.parse(path) 69 | if doc is None: 70 | continue 71 | for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): 72 | lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' 73 | goodness = 1 + (lang in xdg.Locale.langs) 74 | if goodness > self._comment[0]: 75 | self._comment = (goodness, _get_node_data(comment)) 76 | if goodness == 2: return 77 | 78 | # FIXME: add get_icon method 79 | def get_comment(self): 80 | """Returns comment for current language, loading it if needed.""" 81 | # Should we ever reload? 82 | if self._comment is None: 83 | self._comment = (0, str(self)) 84 | self._load() 85 | return self._comment[1] 86 | 87 | def __str__(self): 88 | return self.media + '/' + self.subtype 89 | 90 | def __repr__(self): 91 | return '[%s: %s]' % (self, self._comment or '(comment not loaded)') 92 | 93 | class MagicRule: 94 | def __init__(self, f): 95 | self.next=None 96 | self.prev=None 97 | 98 | #print line 99 | ind='' 100 | while True: 101 | c=f.read(1) 102 | if c=='>': 103 | break 104 | ind+=c 105 | if not ind: 106 | self.nest=0 107 | else: 108 | self.nest=int(ind) 109 | 110 | start='' 111 | while True: 112 | c=f.read(1) 113 | if c=='=': 114 | break 115 | start+=c 116 | self.start=int(start) 117 | 118 | hb=f.read(1) 119 | lb=f.read(1) 120 | self.lenvalue=ord(lb)+(ord(hb)<<8) 121 | 122 | self.value=f.read(self.lenvalue) 123 | 124 | c=f.read(1) 125 | if c=='&': 126 | self.mask=f.read(self.lenvalue) 127 | c=f.read(1) 128 | else: 129 | self.mask=None 130 | 131 | if c=='~': 132 | w='' 133 | while c!='+' and c!='\n': 134 | c=f.read(1) 135 | if c=='+' or c=='\n': 136 | break 137 | w+=c 138 | 139 | self.word=int(w) 140 | else: 141 | self.word=1 142 | 143 | if c=='+': 144 | r='' 145 | while c!='\n': 146 | c=f.read(1) 147 | if c=='\n': 148 | break 149 | r+=c 150 | #print r 151 | self.range=int(r) 152 | else: 153 | self.range=1 154 | 155 | if c!='\n': 156 | raise 'Malformed MIME magic line' 157 | 158 | def getLength(self): 159 | return self.start+self.lenvalue+self.range 160 | 161 | def appendRule(self, rule): 162 | if self.nest%d=[%d]%s&%s~%d+%d>' % (self.nest, 195 | self.start, 196 | self.lenvalue, 197 | `self.value`, 198 | `self.mask`, 199 | self.word, 200 | self.range) 201 | 202 | class MagicType: 203 | def __init__(self, mtype): 204 | self.mtype=mtype 205 | self.top_rules=[] 206 | self.last_rule=None 207 | 208 | def getLine(self, f): 209 | nrule=MagicRule(f) 210 | 211 | if nrule.nest and self.last_rule: 212 | self.last_rule.appendRule(nrule) 213 | else: 214 | self.top_rules.append(nrule) 215 | 216 | self.last_rule=nrule 217 | 218 | return nrule 219 | 220 | def match(self, buffer): 221 | for rule in self.top_rules: 222 | if rule.match(buffer): 223 | return self.mtype 224 | 225 | def __repr__(self): 226 | return '' % self.mtype 227 | 228 | class MagicDB: 229 | def __init__(self): 230 | self.types={} # Indexed by priority, each entry is a list of type rules 231 | self.maxlen=0 232 | 233 | def mergeFile(self, fname): 234 | f=file(fname, 'r') 235 | line=f.readline() 236 | if line!='MIME-Magic\0\n': 237 | raise 'Not a MIME magic file' 238 | 239 | while True: 240 | shead=f.readline() 241 | #print shead 242 | if not shead: 243 | break 244 | if shead[0]!='[' or shead[-2:]!=']\n': 245 | raise 'Malformed section heading' 246 | pri, tname=shead[1:-2].split(':') 247 | #print shead[1:-2] 248 | pri=int(pri) 249 | mtype=lookup(tname) 250 | 251 | try: 252 | ents=self.types[pri] 253 | except: 254 | ents=[] 255 | self.types[pri]=ents 256 | 257 | magictype=MagicType(mtype) 258 | #print tname 259 | 260 | #rline=f.readline() 261 | c=f.read(1) 262 | f.seek(-1, 1) 263 | while c and c!='[': 264 | rule=magictype.getLine(f) 265 | #print rule 266 | if rule and rule.getLength()>self.maxlen: 267 | self.maxlen=rule.getLength() 268 | 269 | c=f.read(1) 270 | f.seek(-1, 1) 271 | 272 | ents.append(magictype) 273 | #self.types[pri]=ents 274 | if not c: 275 | break 276 | 277 | def match_data(self, data, max_pri=100, min_pri=0): 278 | pris=self.types.keys() 279 | pris.sort(lambda a, b: -cmp(a, b)) 280 | for pri in pris: 281 | #print pri, max_pri, min_pri 282 | if pri>max_pri: 283 | continue 284 | if pri' % self.types 303 | 304 | 305 | # Some well-known types 306 | text = lookup('text', 'plain') 307 | inode_block = lookup('inode', 'blockdevice') 308 | inode_char = lookup('inode', 'chardevice') 309 | inode_dir = lookup('inode', 'directory') 310 | inode_fifo = lookup('inode', 'fifo') 311 | inode_socket = lookup('inode', 'socket') 312 | inode_symlink = lookup('inode', 'symlink') 313 | inode_door = lookup('inode', 'door') 314 | app_exe = lookup('application', 'executable') 315 | 316 | _cache_uptodate = False 317 | 318 | def _cache_database(): 319 | global exts, globs, literals, magic, _cache_uptodate 320 | 321 | _cache_uptodate = True 322 | 323 | exts = {} # Maps extensions to types 324 | globs = [] # List of (glob, type) pairs 325 | literals = {} # Maps liternal names to types 326 | magic = MagicDB() 327 | 328 | def _import_glob_file(path): 329 | """Loads name matching information from a MIME directory.""" 330 | for line in file(path): 331 | if line.startswith('#'): continue 332 | line = line[:-1] 333 | 334 | type_name, pattern = line.split(':', 1) 335 | mtype = lookup(type_name) 336 | 337 | if pattern.startswith('*.'): 338 | rest = pattern[2:] 339 | if not ('*' in rest or '[' in rest or '?' in rest): 340 | exts[rest] = mtype 341 | continue 342 | if '*' in pattern or '[' in pattern or '?' in pattern: 343 | globs.append((pattern, mtype)) 344 | else: 345 | literals[pattern] = mtype 346 | 347 | for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')): 348 | _import_glob_file(path) 349 | for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): 350 | magic.mergeFile(path) 351 | 352 | # Sort globs by length 353 | globs.sort(lambda a, b: cmp(len(b[0]), len(a[0]))) 354 | 355 | def get_type_by_name(path): 356 | """Returns type of file by its name, or None if not known""" 357 | if not _cache_uptodate: 358 | _cache_database() 359 | 360 | leaf = os.path.basename(path) 361 | if leaf in literals: 362 | return literals[leaf] 363 | 364 | lleaf = leaf.lower() 365 | if lleaf in literals: 366 | return literals[lleaf] 367 | 368 | ext = leaf 369 | while 1: 370 | p = ext.find('.') 371 | if p < 0: break 372 | ext = ext[p + 1:] 373 | if ext in exts: 374 | return exts[ext] 375 | ext = lleaf 376 | while 1: 377 | p = ext.find('.') 378 | if p < 0: break 379 | ext = ext[p+1:] 380 | if ext in exts: 381 | return exts[ext] 382 | for (glob, mime_type) in globs: 383 | if fnmatch.fnmatch(leaf, glob): 384 | return mime_type 385 | if fnmatch.fnmatch(lleaf, glob): 386 | return mime_type 387 | return None 388 | 389 | def get_type_by_contents(path, max_pri=100, min_pri=0): 390 | """Returns type of file by its contents, or None if not known""" 391 | if not _cache_uptodate: 392 | _cache_database() 393 | 394 | return magic.match(path, max_pri, min_pri) 395 | 396 | def get_type_by_data(data, max_pri=100, min_pri=0): 397 | """Returns type of the data""" 398 | if not _cache_uptodate: 399 | _cache_database() 400 | 401 | return magic.match_data(data, max_pri, min_pri) 402 | 403 | def get_type(path, follow=1, name_pri=100): 404 | """Returns type of file indicated by path. 405 | path - pathname to check (need not exist) 406 | follow - when reading file, follow symbolic links 407 | name_pri - Priority to do name matches. 100=override magic""" 408 | if not _cache_uptodate: 409 | _cache_database() 410 | 411 | try: 412 | if follow: 413 | st = os.stat(path) 414 | else: 415 | st = os.lstat(path) 416 | except: 417 | t = get_type_by_name(path) 418 | return t or text 419 | 420 | if stat.S_ISREG(st.st_mode): 421 | t = get_type_by_contents(path, min_pri=name_pri) 422 | if not t: t = get_type_by_name(path) 423 | if not t: t = get_type_by_contents(path, max_pri=name_pri) 424 | if t is None: 425 | if stat.S_IMODE(st.st_mode) & 0111: 426 | return app_exe 427 | else: 428 | return text 429 | return t 430 | elif stat.S_ISDIR(st.st_mode): return inode_dir 431 | elif stat.S_ISCHR(st.st_mode): return inode_char 432 | elif stat.S_ISBLK(st.st_mode): return inode_block 433 | elif stat.S_ISFIFO(st.st_mode): return inode_fifo 434 | elif stat.S_ISLNK(st.st_mode): return inode_symlink 435 | elif stat.S_ISSOCK(st.st_mode): return inode_socket 436 | return inode_door 437 | 438 | def install_mime_info(application, package_file): 439 | """Copy 'package_file' as ~/.local/share/mime/packages/.xml. 440 | If package_file is None, install /.xml. 441 | If already installed, does nothing. May overwrite an existing 442 | file with the same name (if the contents are different)""" 443 | application += '.xml' 444 | 445 | new_data = file(package_file).read() 446 | 447 | # See if the file is already installed 448 | package_dir = os.path.join('mime', 'packages') 449 | resource = os.path.join(package_dir, application) 450 | for x in xdg.BaseDirectory.load_data_paths(resource): 451 | try: 452 | old_data = file(x).read() 453 | except: 454 | continue 455 | if old_data == new_data: 456 | return # Already installed 457 | 458 | global _cache_uptodate 459 | _cache_uptodate = False 460 | 461 | # Not already installed; add a new copy 462 | # Create the directory structure... 463 | new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application) 464 | 465 | # Write the file... 466 | file(new_file, 'w').write(new_data) 467 | 468 | # Update the database... 469 | command = 'update-mime-database' 470 | if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')): 471 | os.unlink(new_file) 472 | raise Exception("The '%s' command returned an error code!\n" \ 473 | "Make sure you have the freedesktop.org shared MIME package:\n" \ 474 | "http://standards.freedesktop.org/shared-mime-info/") % command 475 | -------------------------------------------------------------------------------- /core/xdg/DesktopEntry.py: -------------------------------------------------------------------------------- 1 | """ 2 | Complete implementation of the XDG Desktop Entry Specification Version 0.9.4 3 | http://standards.freedesktop.org/desktop-entry-spec/ 4 | 5 | Not supported: 6 | - Encoding: Legacy Mixed 7 | - Does not check exec parameters 8 | - Does not check URL's 9 | - Does not completly validate deprecated/kde items 10 | - Does not completly check categories 11 | """ 12 | 13 | from xdg.IniFile import * 14 | from xdg.BaseDirectory import * 15 | import os.path 16 | 17 | class DesktopEntry(IniFile): 18 | "Class to parse and validate DesktopEntries" 19 | 20 | defaultGroup = 'Desktop Entry' 21 | 22 | def __init__(self, filename=None): 23 | self.content = dict() 24 | if filename and os.path.exists(filename): 25 | self.parse(filename) 26 | elif filename: 27 | self.new(filename) 28 | 29 | def __str__(self): 30 | return self.getName() 31 | 32 | def parse(self, file): 33 | IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) 34 | 35 | # start standard keys 36 | def getType(self): 37 | return self.get('Type') 38 | def getVersion(self): 39 | return self.get('Version', type="numeric") 40 | def getEncoding(self): 41 | return self.get('Encoding') 42 | def getName(self): 43 | return self.get('Name', locale=True) 44 | def getGenericName(self): 45 | return self.get('GenericName', locale=True) 46 | def getComment(self): 47 | return self.get('Comment', locale=True) 48 | def getNoDisplay(self): 49 | return self.get('NoDisplay', type="boolean") 50 | def getIcon(self): 51 | return self.get('Icon', locale=True) 52 | def getHidden(self): 53 | return self.get('Hidden', type="boolean") 54 | def getFilePattern(self): 55 | return self.get('FilePattern', type="regex") 56 | def getTryExec(self): 57 | return self.get('TryExec') 58 | def getExec(self): 59 | return self.get('Exec') 60 | def getPath(self): 61 | return self.get('Path') 62 | def getTerminal(self): 63 | return self.get('Terminal', type="boolean") 64 | def getSwallowTitle(self): 65 | return self.get('SwallowTitle', locale=True) 66 | def getSwallowExec(self): 67 | return self.get('SwallowExec') 68 | def getActions(self): 69 | return self.get('Actions', list=True) 70 | """ @deprecated, use getMimeTypes instead """ 71 | def getMimeType(self): 72 | return self.get('MimeType', list=True, type="regex") 73 | def getMimeTypes(self): 74 | return self.get('MimeType', list=True) 75 | def getSortOrder(self): 76 | return self.get('SortOrder', list=True) 77 | def getDev(self): 78 | return self.get('Dev') 79 | def getFSType(self): 80 | return self.get('FSType') 81 | def getMountPoint(self): 82 | return self.get('MountPoint') 83 | def getReadonly(self): 84 | return self.get('ReadOnly', type="boolean") 85 | def getUnmountIcon(self): 86 | return self.get('UnmountIcon', locale=True) 87 | def getURL(self): 88 | return self.get('URL') 89 | def getCategories(self): 90 | return self.get('Categories', list=True) 91 | def getOnlyShowIn(self): 92 | return self.get('OnlyShowIn', list=True) 93 | def getNotShowIn(self): 94 | return self.get('NotShowIn', list=True) 95 | def getStartupNotify(self): 96 | return self.get('StartupNotify', type="boolean") 97 | def getStartupWMClass(self): 98 | return self.get('StartupWMClass') 99 | # end standard keys 100 | 101 | # start kde keys 102 | def getServiceTypes(self): 103 | return self.get('ServiceTypes', list=True) 104 | def getDocPath(self): 105 | return self.get('DocPath') 106 | def getKeywords(self): 107 | return self.get('Keywords', list=True, locale=True) 108 | def getInitialPreference(self): 109 | return self.get('InitialPreference') 110 | # end kde keys 111 | 112 | # start deprecated keys 113 | def getMiniIcon(self): 114 | return self.get('MiniIcon', locale=True) 115 | def getTerminalOptions(self): 116 | return self.get('TerminalOptions') 117 | def getDefaultApp(self): 118 | return self.get('DefaultApp') 119 | def getProtocols(self): 120 | return self.get('Protocols', list=True) 121 | def getExtensions(self): 122 | return self.get('Extensions', list=True) 123 | def getBinaryPattern(self): 124 | return self.get('BinaryPattern') 125 | def getMapNotify(self): 126 | return self.get('MapNotify') 127 | # end deprecated keys 128 | 129 | # desktop entry edit stuff 130 | def new(self, filename): 131 | if os.path.splitext(filename)[1] == ".desktop": 132 | type = "Application" 133 | elif os.path.splitext(filename)[1] == ".directory": 134 | type = "Directory" 135 | self.content = dict() 136 | self.addGroup(self.defaultGroup) 137 | self.set("Encoding", "UTF-8") 138 | self.set("Type", type) 139 | self.filename = filename 140 | # end desktop entry edit stuff 141 | 142 | # validation stuff 143 | def checkExtras(self): 144 | # header 145 | if self.defaultGroup == "KDE Desktop Entry": 146 | self.warnings.append('[KDE Desktop Entry]-Header is deprecated') 147 | 148 | # file extension 149 | if self.fileExtension == ".kdelnk": 150 | self.warnings.append("File extension .kdelnk is deprecated") 151 | elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": 152 | self.warnings.append('Unknown File extension') 153 | 154 | # Type 155 | try: 156 | self.type = self.content[self.defaultGroup]["Type"] 157 | except KeyError: 158 | self.errors.append("Key 'Type' is missing") 159 | 160 | # Encoding 161 | try: 162 | self.encoding = self.content[self.defaultGroup]["Encoding"] 163 | except KeyError: 164 | self.errors.append("Key 'Encoding' is missing") 165 | 166 | # Version 167 | try: 168 | self.version = self.content[self.defaultGroup]["Version"] 169 | except KeyError: 170 | self.warnings.append("Key 'Version' is missing") 171 | 172 | # Name 173 | try: 174 | self.name = self.content[self.defaultGroup]["Name"] 175 | except KeyError: 176 | self.errors.append("Key 'Name' is missing") 177 | 178 | def checkGroup(self, group): 179 | # check if group header is valid 180 | if not (group == self.defaultGroup \ 181 | or re.match("^\Desktop Action [a-zA-Z]+\$", group) \ 182 | or (re.match("^\X-", group) and group.decode("utf-8", "ignore").encode("ascii", 'ignore') == group)): 183 | self.errors.append("Invalid Group name: %s" % group) 184 | else: 185 | #OnlyShowIn and NotShowIn 186 | if self.content[group].has_key("OnlyShowIn") and self.content[group].has_key("NotShowIn"): 187 | self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") 188 | 189 | def checkKey(self, key, value, group): 190 | # standard keys 191 | if key == "Type": 192 | if value == "ServiceType" or value == "Service": 193 | self.warnings.append("Type=%s is a KDE extension" % key) 194 | elif value == "MimeType": 195 | self.warnings.append("Type=MimeType is deprecated") 196 | elif not (value == "Application" or value == "Link" or value == "FSDevice" or value == "Directory"): 197 | self.errors.append("Value of key 'Type' must be Application, Link, FSDevice or Directory, but is '%s'" % value) 198 | 199 | if self.fileExtension == ".directory" and not value == "Directory": 200 | self.warnings.append("File extension is .directory, but Type is '%s'" % value) 201 | elif self.fileExtension == ".desktop" and value == "Directory": 202 | self.warnings.append("Files with Type=Directory should have the extension .directory") 203 | 204 | elif key == "Version": 205 | self.checkValue(key, value, type="number") 206 | 207 | elif key == "Encoding": 208 | if value == "Legacy-Mixed": 209 | self.errors.append("Encoding=Legacy-Mixed is deprecated and not supported by this parser") 210 | elif not value == "UTF-8": 211 | self.errors.append("Value of key 'Encoding' must be UTF-8") 212 | 213 | elif re.match("^Name"+xdg.Locale.regex+"$", key): 214 | pass # locale string 215 | 216 | elif re.match("^GenericName"+xdg.Locale.regex+"$", key): 217 | pass # locale string 218 | 219 | elif re.match("^Comment"+xdg.Locale.regex+"$", key): 220 | pass # locale string 221 | 222 | elif key == "NoDisplay": 223 | self.checkValue(key, value, type="boolean") 224 | 225 | elif key == "Hidden": 226 | self.checkValue(key, value, type="boolean") 227 | 228 | elif key == "Terminal": 229 | self.checkValue(key, value, type="boolean") 230 | self.checkType(key, "Application") 231 | 232 | elif key == "TryExec": 233 | self.checkValue(key, value) 234 | self.checkType(key, "Application") 235 | 236 | elif key == "Exec": 237 | self.checkValue(key, value) 238 | self.checkType(key, "Application") 239 | 240 | elif key == "Path": 241 | self.checkValue(key, value) 242 | self.checkType(key, "Application") 243 | 244 | elif re.match("^Icon"+xdg.Locale.regex+"$", key): 245 | self.checkValue(key, value) 246 | 247 | elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): 248 | self.checkType(key, "Application") 249 | 250 | elif key == "SwallowExec": 251 | self.checkValue(key, value) 252 | self.checkType(key, "Application") 253 | 254 | elif key == "FilePatterns": 255 | self.checkValue(key, value, type="regex", list=True) 256 | self.checkType(key, "Application") 257 | 258 | elif key == "Actions": 259 | self.checkValue(key, value, list=True) 260 | self.checkType(key, "Application") 261 | 262 | elif key == "MimeType": 263 | self.checkValue(key, value, type="regex", list=True) 264 | self.checkType(key, "Application") 265 | 266 | elif key == "Categories": 267 | self.checkValue(key, value) 268 | self.checkType(key, "Application") 269 | self.checkCategorie(value) 270 | 271 | elif key == "OnlyShowIn": 272 | self.checkValue(key, value, list=True) 273 | self.checkOnlyShowIn(value) 274 | 275 | elif key == "NotShowIn": 276 | self.checkValue(key, value, list=True) 277 | self.checkOnlyShowIn(value) 278 | 279 | elif key == "StartupNotify": 280 | self.checkValue(key, value, type="boolean") 281 | self.checkType(key, "Application") 282 | 283 | elif key == "StartupWMClass": 284 | self.checkType(key, "Application") 285 | 286 | elif key == "SortOrder": 287 | self.checkValue(key, value, list=True) 288 | self.checkType(key, "Directory") 289 | 290 | elif key == "URL": 291 | self.checkValue(key, value) 292 | self.checkType(key, "URL") 293 | 294 | elif key == "Dev": 295 | self.checkValue(key, value) 296 | self.checkType(key, "FSDevice") 297 | 298 | elif key == "FSType": 299 | self.checkValue(key, value) 300 | self.checkType(key, "FSDevice") 301 | 302 | elif key == "MountPoint": 303 | self.checkValue(key, value) 304 | self.checkType(key, "FSDevice") 305 | 306 | elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): 307 | self.checkValue(key, value) 308 | self.checkType(key, "FSDevice") 309 | 310 | elif key == "ReadOnly": 311 | self.checkValue(key, value, type="boolean") 312 | self.checkType(key, "FSDevice") 313 | 314 | # kde extensions 315 | elif key == "ServiceTypes": 316 | self.checkValue(key, value, list=True) 317 | self.warnings.append("Key '%s' is a KDE extension" % key) 318 | 319 | elif key == "DocPath": 320 | self.checkValue(key, value) 321 | self.warnings.append("Key '%s' is a KDE extension" % key) 322 | 323 | elif re.match("^Keywords"+xdg.Locale.regex+"$", key): 324 | self.checkValue(key, value, list=True) 325 | self.warnings.append("Key '%s' is a KDE extension" % key) 326 | 327 | elif key == "InitialPreference": 328 | self.checkValue(key, value, type="number") 329 | self.warnings.append("Key '%s' is a KDE extension" % key) 330 | 331 | # deprecated keys 332 | elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): 333 | self.checkValue(key, value) 334 | self.warnings.append("Key '%s' is deprecated" % key) 335 | 336 | elif key == "TerminalOptions": 337 | self.checkValue(key, value) 338 | self.warnings.append("Key '%s' is deprecated" % key) 339 | 340 | elif key == "DefaultApp": 341 | self.checkValue(key, value) 342 | self.warnings.append("Key '%s' is deprecated" % key) 343 | 344 | elif key == "Protocols": 345 | self.checkValue(key, value, list=True) 346 | self.warnings.append("Key '%s' is deprecated" % key) 347 | 348 | elif key == "Extensions": 349 | self.checkValue(key, value, list=True) 350 | self.warnings.append("Key '%s' is deprecated" % key) 351 | 352 | elif key == "BinaryPattern": 353 | self.checkValue(key, value) 354 | self.warnings.append("Key '%s' is deprecated" % key) 355 | 356 | elif key == "MapNotify": 357 | self.checkValue(key, value) 358 | self.warnings.append("Key '%s' is deprecated" % key) 359 | 360 | # "X-" extensions 361 | elif re.match("^X-[a-zA-Z0-9-]+", key): 362 | pass 363 | 364 | else: 365 | self.errors.append("Invalid key: %s" % key) 366 | 367 | def checkType(self, key, type): 368 | if not self.getType() == type: 369 | self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) 370 | 371 | def checkOnlyShowIn(self, value): 372 | values = self.getList(value) 373 | valid = ["GNOME", "KDE", "ROX", "XFCE", "Old"] 374 | for item in values: 375 | if item not in valid: 376 | self.errors.append("'%s' is not a registered OnlyShowIn value" % item); 377 | 378 | def checkCategorie(self, value): 379 | values = self.getList(value) 380 | valid = ["Legacy","Core","Development","Building","Debugger","IDE","GUIDesigner","Profiling","RevisionControl","Translation","Office","Calendar","ContactManagement","Database","Dictionary","Chart","Email","Finance","FlowChart","PDA","ProjectManagement","Presentation","Spreadsheet","WordProcessor","Graphics","2DGraphics","VectorGraphics","RasterGraphics","3DGraphics","Scanning","OCR","Photograph","Viewer","Settings","DesktopSettings","HardwareSettings","PackageManager","Network","Dialup","InstantMessaging","IRCClient","FileTransfer","HamRadio","News","P2P","RemoteAccess","Telephony","WebBrowser","WebDevelopment","AudioVideo","Audio","Midi","Mixer","Sequencer","Tuner","Video","TV","AudioVideoEditing","Player","Recorder","DiscBurning","Game","ActionGame","AdventureGame","ArcadeGame","BoardGame","BlocksGame","CardGame","KidsGame","LogicGame","RolePlaying","Simulation","SportsGame","StrategyGame","Education","Art","Art","Contruction","Music","Languages","Science","Astronomy","Biology","Chemistry","Geology","Math","MedicalSoftware","Physics","Teaching","Amusement","Applet","Archiving","Electronics","Emulator","Engineering","FileManager","Shell","Screensaver","TerminalEmulator","TrayIcon","System","Filesystem","Monitor","Security","Utility","Accessibility","Calculator","Clock","TextEditor","KDE","GNOME","GTK","Qt","Motif","Java","ConsoleOnly"] 381 | for item in values: 382 | if item not in valid: 383 | self.errors.append("'%s' is not a registered Category" % item); 384 | -------------------------------------------------------------------------------- /core/xdg/MenuEditor.py: -------------------------------------------------------------------------------- 1 | """ CLass to edit XDG Menus """ 2 | 3 | from xdg.Menu import * 4 | from xdg.BaseDirectory import * 5 | from xdg.Exceptions import * 6 | from xdg.DesktopEntry import * 7 | from xdg.Config import * 8 | 9 | import xml.dom.minidom 10 | import os 11 | import re 12 | 13 | # XML-Cleanups: Move / Exclude 14 | # FIXME: proper reverte/delete 15 | # FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions 16 | # FIXME: catch Exceptions 17 | # FIXME: copy functions 18 | # FIXME: More Layout stuff 19 | # FIXME: unod/redo function / remove menu... 20 | # FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile 21 | # Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs 22 | 23 | class MenuEditor: 24 | def __init__(self, menu=None, filename=None, root=False): 25 | self.menu = None 26 | self.filename = None 27 | self.doc = None 28 | self.parse(menu, filename, root) 29 | 30 | # fix for creating two menus with the same name on the fly 31 | self.filenames = [] 32 | 33 | def parse(self, menu=None, filename=None, root=False): 34 | if root == True: 35 | setRootMode(True) 36 | 37 | if isinstance(menu, Menu): 38 | self.menu = menu 39 | elif menu: 40 | self.menu = parse(menu) 41 | else: 42 | self.menu = parse() 43 | 44 | if root == True: 45 | self.filename = self.menu.Filename 46 | elif filename: 47 | self.filename = filename 48 | else: 49 | self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) 50 | 51 | try: 52 | self.doc = xml.dom.minidom.parse(self.filename) 53 | except IOError: 54 | self.doc = xml.dom.minidom.parseString('Applications'+self.menu.Filename+'') 55 | except xml.parsers.expat.ExpatError: 56 | raise ParsingError('Not a valid .menu file', self.filename) 57 | 58 | self.__remove_whilespace_nodes(self.doc) 59 | 60 | def save(self): 61 | self.__saveEntries(self.menu) 62 | self.__saveMenu() 63 | 64 | def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): 65 | menuentry = MenuEntry(self.__getFileName(name, ".desktop")) 66 | menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) 67 | 68 | self.__addEntry(parent, menuentry, after, before) 69 | 70 | sort(self.menu) 71 | 72 | return menuentry 73 | 74 | def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): 75 | menu = Menu() 76 | 77 | menu.Parent = parent 78 | menu.Depth = parent.Depth + 1 79 | menu.Layout = parent.DefaultLayout 80 | menu.DefaultLayout = parent.DefaultLayout 81 | 82 | menu = self.editMenu(menu, name, genericname, comment, icon) 83 | 84 | self.__addEntry(parent, menu, after, before) 85 | 86 | sort(self.menu) 87 | 88 | return menu 89 | 90 | def createSeparator(self, parent, after=None, before=None): 91 | separator = Separator(parent) 92 | 93 | self.__addEntry(parent, separator, after, before) 94 | 95 | sort(self.menu) 96 | 97 | return separator 98 | 99 | def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): 100 | self.__deleteEntry(oldparent, menuentry, after, before) 101 | self.__addEntry(newparent, menuentry, after, before) 102 | 103 | sort(self.menu) 104 | 105 | return menuentry 106 | 107 | def moveMenu(self, menu, oldparent, newparent, after=None, before=None): 108 | self.__deleteEntry(oldparent, menu, after, before) 109 | self.__addEntry(newparent, menu, after, before) 110 | 111 | root_menu = self.__getXmlMenu(self.menu.Name) 112 | if oldparent.getPath(True) != newparent.getPath(True): 113 | self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) 114 | 115 | sort(self.menu) 116 | 117 | return menu 118 | 119 | def moveSeparator(self, separator, parent, after=None, before=None): 120 | self.__deleteEntry(parent, separator, after, before) 121 | self.__addEntry(parent, separator, after, before) 122 | 123 | sort(self.menu) 124 | 125 | return separator 126 | 127 | def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): 128 | self.__addEntry(newparent, menuentry, after, before) 129 | 130 | sort(self.menu) 131 | 132 | return menuentry 133 | 134 | def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): 135 | deskentry = menuentry.DesktopEntry 136 | 137 | if name: 138 | if not deskentry.hasKey("Name"): 139 | deskentry.set("Name", name) 140 | deskentry.set("Name", name, locale = True) 141 | if comment: 142 | if not deskentry.hasKey("Comment"): 143 | deskentry.set("Comment", comment) 144 | deskentry.set("Comment", comment, locale = True) 145 | if genericname: 146 | if not deskentry.hasKey("GnericNe"): 147 | deskentry.set("GenericName", genericname) 148 | deskentry.set("GenericName", genericname, locale = True) 149 | if command: 150 | deskentry.set("Exec", command) 151 | if icon: 152 | deskentry.set("Icon", icon) 153 | 154 | if terminal == True: 155 | deskentry.set("Terminal", "true") 156 | elif terminal == False: 157 | deskentry.set("Terminal", "false") 158 | 159 | if nodisplay == True: 160 | deskentry.set("NoDisplay", "true") 161 | elif nodisplay == False: 162 | deskentry.set("NoDisplay", "false") 163 | 164 | if hidden == True: 165 | deskentry.set("Hidden", "true") 166 | elif hidden == False: 167 | deskentry.set("Hidden", "false") 168 | 169 | menuentry.updateAttributes() 170 | 171 | if len(menuentry.Parents) > 0: 172 | sort(self.menu) 173 | 174 | return menuentry 175 | 176 | def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): 177 | # Hack for legacy dirs 178 | if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": 179 | xml_menu = self.__getXmlMenu(menu.getPath(True, True)) 180 | self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") 181 | menu.Directory.setAttributes(menu.Name + ".directory") 182 | # Hack for New Entries 183 | elif not isinstance(menu.Directory, MenuEntry): 184 | if not name: 185 | name = menu.Name 186 | filename = self.__getFileName(name, ".directory").replace("/", "") 187 | if not menu.Name: 188 | menu.Name = filename.replace(".directory", "") 189 | xml_menu = self.__getXmlMenu(menu.getPath(True, True)) 190 | self.__addXmlTextElement(xml_menu, 'Directory', filename) 191 | menu.Directory = MenuEntry(filename) 192 | 193 | deskentry = menu.Directory.DesktopEntry 194 | 195 | if name: 196 | if not deskentry.hasKey("Name"): 197 | deskentry.set("Name", name) 198 | deskentry.set("Name", name, locale = True) 199 | if genericname: 200 | if not deskentry.hasKey("GenericName"): 201 | deskentry.set("GenericName", genericname) 202 | deskentry.set("GenericName", genericname, locale = True) 203 | if comment: 204 | if not deskentry.hasKey("Comment"): 205 | deskentry.set("Comment", comment) 206 | deskentry.set("Comment", comment, locale = True) 207 | if icon: 208 | deskentry.set("Icon", icon) 209 | 210 | if nodisplay == True: 211 | deskentry.set("NoDisplay", "true") 212 | elif nodisplay == False: 213 | deskentry.set("NoDisplay", "false") 214 | 215 | if hidden == True: 216 | deskentry.set("Hidden", "true") 217 | elif hidden == False: 218 | deskentry.set("Hidden", "false") 219 | 220 | menu.Directory.updateAttributes() 221 | 222 | if isinstance(menu.Parent, Menu): 223 | sort(self.menu) 224 | 225 | return menu 226 | 227 | def hideMenuEntry(self, menuentry): 228 | self.editMenuEntry(menuentry, nodisplay = True) 229 | 230 | def unhideMenuEntry(self, menuentry): 231 | self.editMenuEntry(menuentry, nodisplay = False, hidden = False) 232 | 233 | def hideMenu(self, menu): 234 | self.editMenu(menu, nodisplay = True) 235 | 236 | def unhideMenu(self, menu): 237 | self.editMenu(menu, nodisplay = False, hidden = False) 238 | xml_menu = self.__getXmlMenu(menu.getPath(True,True), False) 239 | for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu): 240 | node.parentNode.removeChild(node) 241 | 242 | def deleteMenuEntry(self, menuentry): 243 | if self.getAction(menuentry) == "delete": 244 | self.__deleteFile(menuentry.DesktopEntry.filename) 245 | for parent in menuentry.Parents: 246 | self.__deleteEntry(parent, menuentry) 247 | sort(self.menu) 248 | return menuentry 249 | 250 | def revertMenuEntry(self, menuentry): 251 | if self.getAction(menuentry) == "revert": 252 | self.__deleteFile(menuentry.DesktopEntry.filename) 253 | menuentry.Original.Parents = [] 254 | for parent in menuentry.Parents: 255 | index = parent.Entries.index(menuentry) 256 | parent.Entries[index] = menuentry.Original 257 | index = parent.MenuEntries.index(menuentry) 258 | parent.MenuEntries[index] = menuentry.Original 259 | menuentry.Original.Parents.append(parent) 260 | sort(self.menu) 261 | return menuentry 262 | 263 | def deleteMenu(self, menu): 264 | if self.getAction(menu) == "delete": 265 | self.__deleteFile(menu.Directory.DesktopEntry.filename) 266 | self.__deleteEntry(menu.Parent, menu) 267 | xml_menu = self.__getXmlMenu(menu.getPath(True, True)) 268 | xml_menu.parentNode.removeChild(xml_menu) 269 | sort(self.menu) 270 | return menu 271 | 272 | def revertMenu(self, menu): 273 | if self.getAction(menu) == "revert": 274 | self.__deleteFile(menu.Directory.DesktopEntry.filename) 275 | menu.Directory = menu.Directory.Original 276 | sort(self.menu) 277 | return menu 278 | 279 | def deleteSeparator(self, separator): 280 | self.__deleteEntry(separator.Parent, separator, after=True) 281 | 282 | sort(self.menu) 283 | 284 | return separator 285 | 286 | """ Private Stuff """ 287 | def getAction(self, entry): 288 | if isinstance(entry, Menu): 289 | if not isinstance(entry.Directory, MenuEntry): 290 | return "none" 291 | elif entry.Directory.getType() == "Both": 292 | return "revert" 293 | elif entry.Directory.getType() == "User" \ 294 | and (len(entry.Submenus) + len(entry.MenuEntries)) == 0: 295 | return "delete" 296 | 297 | elif isinstance(entry, MenuEntry): 298 | if entry.getType() == "Both": 299 | return "revert" 300 | elif entry.getType() == "User": 301 | return "delete" 302 | else: 303 | return "none" 304 | 305 | return "none" 306 | 307 | def __saveEntries(self, menu): 308 | if not menu: 309 | menu = self.menu 310 | if isinstance(menu.Directory, MenuEntry): 311 | menu.Directory.save() 312 | for entry in menu.getEntries(hidden=True): 313 | if isinstance(entry, MenuEntry): 314 | entry.save() 315 | elif isinstance(entry, Menu): 316 | self.__saveEntries(entry) 317 | 318 | def __saveMenu(self): 319 | if not os.path.isdir(os.path.dirname(self.filename)): 320 | os.makedirs(os.path.dirname(self.filename)) 321 | fd = open(self.filename, 'w') 322 | fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*\n', ''))) 323 | fd.close() 324 | 325 | def __getFileName(self, name, extension): 326 | postfix = 0 327 | while 1: 328 | if postfix == 0: 329 | filename = name + extension 330 | else: 331 | filename = name + "-" + str(postfix) + extension 332 | if extension == ".desktop": 333 | dir = "applications" 334 | elif extension == ".directory": 335 | dir = "desktop-directories" 336 | if not filename in self.filenames and not \ 337 | os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)): 338 | self.filenames.append(filename) 339 | break 340 | else: 341 | postfix += 1 342 | 343 | return filename 344 | 345 | def __getXmlMenu(self, path, create=True, element=None): 346 | if not element: 347 | element = self.doc 348 | 349 | if "/" in path: 350 | (name, path) = path.split("/", 1) 351 | else: 352 | name = path 353 | path = "" 354 | 355 | found = None 356 | for node in self.__getXmlNodesByName("Menu", element): 357 | for child in self.__getXmlNodesByName("Name", node): 358 | if child.childNodes[0].nodeValue == name: 359 | if path: 360 | found = self.__getXmlMenu(path, create, node) 361 | else: 362 | found = node 363 | break 364 | if found: 365 | break 366 | if not found and create == True: 367 | node = self.__addXmlMenuElement(element, name) 368 | if path: 369 | found = self.__getXmlMenu(path, create, node) 370 | else: 371 | found = node 372 | 373 | return found 374 | 375 | def __addXmlMenuElement(self, element, name): 376 | node = self.doc.createElement('Menu') 377 | self.__addXmlTextElement(node, 'Name', name) 378 | return element.appendChild(node) 379 | 380 | def __addXmlTextElement(self, element, name, text): 381 | node = self.doc.createElement(name) 382 | text = self.doc.createTextNode(text) 383 | node.appendChild(text) 384 | return element.appendChild(node) 385 | 386 | def __addXmlFilename(self, element, filename, type = "Include"): 387 | # remove old filenames 388 | for node in self.__getXmlNodesByName(["Include", "Exclude"], element): 389 | if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename: 390 | element.removeChild(node) 391 | 392 | # add new filename 393 | node = self.doc.createElement(type) 394 | node.appendChild(self.__addXmlTextElement(node, 'Filename', filename)) 395 | return element.appendChild(node) 396 | 397 | def __addXmlMove(self, element, old, new): 398 | node = self.doc.createElement("Move") 399 | node.appendChild(self.__addXmlTextElement(node, 'Old', old)) 400 | node.appendChild(self.__addXmlTextElement(node, 'New', new)) 401 | return element.appendChild(node) 402 | 403 | def __addXmlLayout(self, element, layout): 404 | # remove old layout 405 | for node in self.__getXmlNodesByName("Layout", element): 406 | element.removeChild(node) 407 | 408 | # add new layout 409 | node = self.doc.createElement("Layout") 410 | for order in layout.order: 411 | if order[0] == "Separator": 412 | child = self.doc.createElement("Separator") 413 | node.appendChild(child) 414 | elif order[0] == "Filename": 415 | child = self.__addXmlTextElement(node, "Filename", order[1]) 416 | elif order[0] == "Menuname": 417 | child = self.__addXmlTextElement(node, "Menuname", order[1]) 418 | elif order[0] == "Merge": 419 | child = self.doc.createElement("Merge") 420 | child.setAttribute("type", order[1]) 421 | node.appendChild(child) 422 | return element.appendChild(node) 423 | 424 | def __getXmlNodesByName(self, name, element): 425 | for child in element.childNodes: 426 | if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name: 427 | yield child 428 | 429 | def __addLayout(self, parent): 430 | layout = Layout() 431 | layout.order = [] 432 | layout.show_empty = parent.Layout.show_empty 433 | layout.inline = parent.Layout.inline 434 | layout.inline_header = parent.Layout.inline_header 435 | layout.inline_alias = parent.Layout.inline_alias 436 | layout.inline_limit = parent.Layout.inline_limit 437 | 438 | layout.order.append(["Merge", "menus"]) 439 | for entry in parent.Entries: 440 | if isinstance(entry, Menu): 441 | layout.parseMenuname(entry.Name) 442 | elif isinstance(entry, MenuEntry): 443 | layout.parseFilename(entry.DesktopFileID) 444 | elif isinstance(entry, Separator): 445 | layout.parseSeparator() 446 | layout.order.append(["Merge", "files"]) 447 | 448 | parent.Layout = layout 449 | 450 | return layout 451 | 452 | def __addEntry(self, parent, entry, after=None, before=None): 453 | if after or before: 454 | if after: 455 | index = parent.Entries.index(after) + 1 456 | elif before: 457 | index = parent.Entries.index(before) 458 | parent.Entries.insert(index, entry) 459 | else: 460 | parent.Entries.append(entry) 461 | 462 | xml_parent = self.__getXmlMenu(parent.getPath(True, True)) 463 | 464 | if isinstance(entry, MenuEntry): 465 | parent.MenuEntries.append(entry) 466 | entry.Parents.append(parent) 467 | self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") 468 | elif isinstance(entry, Menu): 469 | parent.addSubmenu(entry) 470 | 471 | if after or before: 472 | self.__addLayout(parent) 473 | self.__addXmlLayout(xml_parent, parent.Layout) 474 | 475 | def __deleteEntry(self, parent, entry, after=None, before=None): 476 | parent.Entries.remove(entry) 477 | 478 | xml_parent = self.__getXmlMenu(parent.getPath(True, True)) 479 | 480 | if isinstance(entry, MenuEntry): 481 | entry.Parents.remove(parent) 482 | parent.MenuEntries.remove(entry) 483 | self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") 484 | elif isinstance(entry, Menu): 485 | parent.Submenus.remove(entry) 486 | 487 | if after or before: 488 | self.__addLayout(parent) 489 | self.__addXmlLayout(xml_parent, parent.Layout) 490 | 491 | def __deleteFile(self, filename): 492 | try: 493 | os.remove(filename) 494 | except OSError: 495 | pass 496 | try: 497 | self.filenames.remove(filename) 498 | except ValueError: 499 | pass 500 | 501 | def __remove_whilespace_nodes(self, node): 502 | remove_list = [] 503 | for child in node.childNodes: 504 | if child.nodeType == xml.dom.minidom.Node.TEXT_NODE: 505 | child.data = child.data.strip() 506 | if not child.data.strip(): 507 | remove_list.append(child) 508 | elif child.hasChildNodes(): 509 | self.__remove_whilespace_nodes(child) 510 | for node in remove_list: 511 | node.parentNode.removeChild(node) 512 | -------------------------------------------------------------------------------- /core/xdg/Menu.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the XDG Menu Specification Version 1.0.draft-1 3 | http://standards.freedesktop.org/menu-spec/ 4 | """ 5 | 6 | from __future__ import generators 7 | import locale, os, xml.dom.minidom 8 | 9 | from xdg.BaseDirectory import * 10 | from xdg.DesktopEntry import * 11 | from xdg.Exceptions import * 12 | 13 | import xdg.Locale 14 | import xdg.Config 15 | 16 | ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE 17 | 18 | # for python <= 2.3 19 | try: 20 | reversed = reversed 21 | except NameError: 22 | def reversed(x): 23 | return x[::-1] 24 | 25 | class Menu: 26 | def __init__(self): 27 | # Public stuff 28 | self.Name = "" 29 | self.Directory = None 30 | self.Entries = [] 31 | self.Doc = "" 32 | self.Filename = "" 33 | self.Depth = 0 34 | self.Parent = None 35 | self.NotInXml = False 36 | 37 | # Can be one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True 38 | self.Show = True 39 | self.Visible = 0 40 | 41 | # Private stuff, only needed for parsing 42 | self.AppDirs = [] 43 | self.DefaultLayout = None 44 | self.Deleted = "notset" 45 | self.Directories = [] 46 | self.DirectoryDirs = [] 47 | self.Layout = None 48 | self.MenuEntries = [] 49 | self.Moves = [] 50 | self.OnlyUnallocated = "notset" 51 | self.Rules = [] 52 | self.Submenus = [] 53 | 54 | def __str__(self): 55 | return self.Name 56 | 57 | def __add__(self, other): 58 | for dir in other.AppDirs: 59 | self.AppDirs.append(dir) 60 | 61 | for dir in other.DirectoryDirs: 62 | self.DirectoryDirs.append(dir) 63 | 64 | for directory in other.Directories: 65 | self.Directories.append(directory) 66 | 67 | if other.Deleted != "notset": 68 | self.Deleted = other.Deleted 69 | 70 | if other.OnlyUnallocated != "notset": 71 | self.OnlyUnallocated = other.OnlyUnallocated 72 | 73 | if other.Layout: 74 | self.Layout = other.Layout 75 | 76 | if other.DefaultLayout: 77 | self.DefaultLayout = other.DefaultLayout 78 | 79 | for rule in other.Rules: 80 | self.Rules.append(rule) 81 | 82 | for move in other.Moves: 83 | self.Moves.append(move) 84 | 85 | for submenu in other.Submenus: 86 | self.addSubmenu(submenu) 87 | 88 | return self 89 | 90 | # FIXME: Performance: cache getName() 91 | def __cmp__(self, other): 92 | return locale.strcoll(self.getName(), other.getName()) 93 | 94 | def __eq__(self, other): 95 | if self.Name == str(other): 96 | return True 97 | else: 98 | return False 99 | 100 | """ PUBLIC STUFF """ 101 | def getEntries(self, hidden=False): 102 | for entry in self.Entries: 103 | if hidden == True: 104 | yield entry 105 | elif entry.Show == True: 106 | yield entry 107 | 108 | # FIXME: Add searchEntry/seaqrchMenu function 109 | # search for name/comment/genericname/desktopfileide 110 | # return multiple items 111 | 112 | def getMenuEntry(self, desktopfileid, deep = False): 113 | for menuentry in self.MenuEntries: 114 | if menuentry.DesktopFileID == desktopfileid: 115 | return menuentry 116 | if deep == True: 117 | for submenu in self.Submenus: 118 | submenu.getMenuEntry(desktopfileid, deep) 119 | 120 | def getMenu(self, path): 121 | array = path.split("/", 1) 122 | for submenu in self.Submenus: 123 | if submenu.Name == array[0]: 124 | if len(array) > 1: 125 | return submenu.getMenu(array[1]) 126 | else: 127 | return submenu 128 | 129 | def getPath(self, org=False, toplevel=False): 130 | parent = self 131 | names=[] 132 | while 1: 133 | if org: 134 | names.append(parent.Name) 135 | else: 136 | names.append(parent.getName()) 137 | if parent.Depth > 0: 138 | parent = parent.Parent 139 | else: 140 | break 141 | names.reverse() 142 | path = "" 143 | if toplevel == False: 144 | names.pop(0) 145 | for name in names: 146 | path = os.path.join(path, name) 147 | return path 148 | 149 | def getName(self): 150 | try: 151 | return self.Directory.DesktopEntry.getName() 152 | except AttributeError: 153 | return self.Name 154 | 155 | def getGenericName(self): 156 | try: 157 | return self.Directory.DesktopEntry.getGenericName() 158 | except AttributeError: 159 | return "" 160 | 161 | def getComment(self): 162 | try: 163 | return self.Directory.DesktopEntry.getComment() 164 | except AttributeError: 165 | return "" 166 | 167 | def getIcon(self): 168 | try: 169 | return self.Directory.DesktopEntry.getIcon() 170 | except AttributeError: 171 | return "" 172 | 173 | """ PRIVATE STUFF """ 174 | def addSubmenu(self, newmenu): 175 | for submenu in self.Submenus: 176 | if submenu == newmenu: 177 | submenu += newmenu 178 | break 179 | else: 180 | self.Submenus.append(newmenu) 181 | newmenu.Parent = self 182 | newmenu.Depth = self.Depth + 1 183 | 184 | class Move: 185 | "A move operation" 186 | def __init__(self, node=None): 187 | if node: 188 | self.parseNode(node) 189 | else: 190 | self.Old = "" 191 | self.New = "" 192 | 193 | def __cmp__(self, other): 194 | return cmp(self.Old, other.Old) 195 | 196 | def parseNode(self, node): 197 | for child in node.childNodes: 198 | if child.nodeType == ELEMENT_NODE: 199 | if child.tagName == "Old": 200 | try: 201 | self.parseOld(child.childNodes[0].nodeValue) 202 | except IndexError: 203 | raise ValidationError('Old cannot be empty', '??') 204 | elif child.tagName == "New": 205 | try: 206 | self.parseNew(child.childNodes[0].nodeValue) 207 | except IndexError: 208 | raise ValidationError('New cannot be empty', '??') 209 | 210 | def parseOld(self, value): 211 | self.Old = value 212 | def parseNew(self, value): 213 | self.New = value 214 | 215 | 216 | class Layout: 217 | "Menu Layout class" 218 | def __init__(self, node=None): 219 | self.order = [] 220 | if node: 221 | self.show_empty = node.getAttribute("show_empty") or "false" 222 | self.inline = node.getAttribute("inline") or "false" 223 | self.inline_limit = node.getAttribute("inline_limit") or 4 224 | self.inline_header = node.getAttribute("inline_header") or "true" 225 | self.inline_alias = node.getAttribute("inline_alias") or "false" 226 | self.inline_limit = int(self.inline_limit) 227 | self.parseNode(node) 228 | else: 229 | self.show_empty = "false" 230 | self.inline = "false" 231 | self.inline_limit = 4 232 | self.inline_header = "true" 233 | self.inline_alias = "false" 234 | self.order.append(["Merge", "menus"]) 235 | self.order.append(["Merge", "files"]) 236 | 237 | def parseNode(self, node): 238 | for child in node.childNodes: 239 | if child.nodeType == ELEMENT_NODE: 240 | if child.tagName == "Menuname": 241 | try: 242 | self.parseMenuname( 243 | child.childNodes[0].nodeValue, 244 | child.getAttribute("show_empty") or "false", 245 | child.getAttribute("inline") or "false", 246 | child.getAttribute("inline_limit") or 4, 247 | child.getAttribute("inline_header") or "true", 248 | child.getAttribute("inline_alias") or "false" ) 249 | except IndexError: 250 | raise ValidationError('Menuname cannot be empty', "") 251 | elif child.tagName == "Separator": 252 | self.parseSeparator() 253 | elif child.tagName == "Filename": 254 | try: 255 | self.parseFilename(child.childNodes[0].nodeValue) 256 | except IndexError: 257 | raise ValidationError('Filename cannot be empty', "") 258 | elif child.tagName == "Merge": 259 | self.parseMerge(child.getAttribute("type") or "all") 260 | 261 | def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"): 262 | self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias]) 263 | self.order[-1][4] = int(self.order[-1][4]) 264 | 265 | def parseSeparator(self): 266 | self.order.append(["Separator"]) 267 | 268 | def parseFilename(self, value): 269 | self.order.append(["Filename", value]) 270 | 271 | def parseMerge(self, type="all"): 272 | self.order.append(["Merge", type]) 273 | 274 | 275 | class Rule: 276 | "Inlcude / Exclude Rules Class" 277 | def __init__(self, type, node=None): 278 | # Type is Include or Exclude 279 | self.Type = type 280 | # Rule is a python expression 281 | self.Rule = "" 282 | 283 | # Private attributes, only needed for parsing 284 | self.Depth = 0 285 | self.Expr = [ "or" ] 286 | self.New = True 287 | 288 | # Begin parsing 289 | if node: 290 | self.parseNode(node) 291 | self.compile() 292 | 293 | def __str__(self): 294 | return self.Rule 295 | 296 | def compile(self): 297 | exec(""" 298 | def do(menuentries, type, run): 299 | for menuentry in menuentries: 300 | if run == 2 and ( menuentry.MatchedInclude == True \ 301 | or menuentry.Allocated == True ): 302 | continue 303 | elif %s: 304 | if type == "Include": 305 | menuentry.Add = True 306 | menuentry.MatchedInclude = True 307 | else: 308 | menuentry.Add = False 309 | return menuentries 310 | """ % self.Rule) in self.__dict__ 311 | 312 | def parseNode(self, node): 313 | for child in node.childNodes: 314 | if child.nodeType == ELEMENT_NODE: 315 | if child.tagName == 'Filename': 316 | try: 317 | self.parseFilename(child.childNodes[0].nodeValue) 318 | except IndexError: 319 | raise ValidationError('Filename cannot be empty', "???") 320 | elif child.tagName == 'Category': 321 | try: 322 | self.parseCategory(child.childNodes[0].nodeValue) 323 | except IndexError: 324 | raise ValidationError('Category cannot be empty', "???") 325 | elif child.tagName == 'All': 326 | self.parseAll() 327 | elif child.tagName == 'And': 328 | self.parseAnd(child) 329 | elif child.tagName == 'Or': 330 | self.parseOr(child) 331 | elif child.tagName == 'Not': 332 | self.parseNot(child) 333 | 334 | def parseNew(self, set=True): 335 | if not self.New: 336 | self.Rule += " " + self.Expr[self.Depth] + " " 337 | if not set: 338 | self.New = True 339 | elif set: 340 | self.New = False 341 | 342 | def parseFilename(self, value): 343 | self.parseNew() 344 | self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'") 345 | 346 | def parseCategory(self, value): 347 | self.parseNew() 348 | self.Rule += "'%s' in menuentry.Categories" % value.strip() 349 | 350 | def parseAll(self): 351 | self.parseNew() 352 | self.Rule += "True" 353 | 354 | def parseAnd(self, node): 355 | self.parseNew(False) 356 | self.Rule += "(" 357 | self.Depth += 1 358 | self.Expr.append("and") 359 | self.parseNode(node) 360 | self.Depth -= 1 361 | self.Expr.pop() 362 | self.Rule += ")" 363 | 364 | def parseOr(self, node): 365 | self.parseNew(False) 366 | self.Rule += "(" 367 | self.Depth += 1 368 | self.Expr.append("or") 369 | self.parseNode(node) 370 | self.Depth -= 1 371 | self.Expr.pop() 372 | self.Rule += ")" 373 | 374 | def parseNot(self, node): 375 | self.parseNew(False) 376 | self.Rule += "not (" 377 | self.Depth += 1 378 | self.Expr.append("or") 379 | self.parseNode(node) 380 | self.Depth -= 1 381 | self.Expr.pop() 382 | self.Rule += ")" 383 | 384 | 385 | class MenuEntry: 386 | "Wrapper for 'Menu Style' Desktop Entries" 387 | def __init__(self, filename, dir="", prefix=""): 388 | # Create entry 389 | self.DesktopEntry = DesktopEntry(os.path.join(dir,filename)) 390 | self.setAttributes(filename, dir, prefix) 391 | 392 | # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True 393 | self.Show = True 394 | 395 | # Semi-Private 396 | self.Original = None 397 | self.Parents = [] 398 | 399 | # Private Stuff 400 | self.Allocated = False 401 | self.Add = False 402 | self.MatchedInclude = False 403 | 404 | # Caching 405 | self.Categories = self.DesktopEntry.getCategories() 406 | 407 | def save(self): 408 | if self.DesktopEntry.tainted == True: 409 | self.DesktopEntry.write() 410 | 411 | def getDir(self): 412 | return self.DesktopEntry.filename.replace(self.Filename, '') 413 | 414 | def getType(self): 415 | # Can be one of System/User/Both 416 | if xdg.Config.root_mode == False: 417 | if self.Original: 418 | return "Both" 419 | elif xdg_data_dirs[0] in self.DesktopEntry.filename: 420 | return "User" 421 | else: 422 | return "System" 423 | else: 424 | return "User" 425 | 426 | def setAttributes(self, filename, dir="", prefix=""): 427 | self.Filename = filename 428 | self.Prefix = prefix 429 | self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-") 430 | 431 | if not os.path.isabs(self.DesktopEntry.filename): 432 | self.__setFilename() 433 | 434 | def updateAttributes(self): 435 | if self.getType() == "System": 436 | self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) 437 | self.__setFilename() 438 | 439 | def __setFilename(self): 440 | if xdg.Config.root_mode == False: 441 | path = xdg_data_dirs[0] 442 | else: 443 | path= xdg_data_dirs[1] 444 | 445 | if self.DesktopEntry.getType() == "Application": 446 | dir = os.path.join(path, "applications") 447 | else: 448 | dir = os.path.join(path, "desktop-directories") 449 | 450 | self.DesktopEntry.filename = os.path.join(dir, self.Filename) 451 | 452 | def __cmp__(self, other): 453 | return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) 454 | 455 | def __eq__(self, other): 456 | if self.DesktopFileID == str(other): 457 | return True 458 | else: 459 | return False 460 | 461 | def __repr__(self): 462 | return self.DesktopFileID 463 | 464 | 465 | class Separator: 466 | "Just a dummy class for Separators" 467 | def __init__(self, parent): 468 | self.Parent = parent 469 | self.Show = True 470 | 471 | 472 | class Header: 473 | "Class for Inline Headers" 474 | def __init__(self, name, generic_name, comment): 475 | self.Name = name 476 | self.GenericName = generic_name 477 | self.Comment = comment 478 | 479 | def __str__(self): 480 | return self.Name 481 | 482 | 483 | tmp = {} 484 | 485 | def __getFileName(filename): 486 | dirs = xdg_config_dirs[:] 487 | if xdg.Config.root_mode == True: 488 | dirs.pop(0) 489 | 490 | for dir in dirs: 491 | menuname = os.path.join (dir, "menus" , filename) 492 | if os.path.isdir(dir) and os.path.isfile(menuname): 493 | return menuname 494 | 495 | def parse(filename=None): 496 | # conver to absolute path 497 | if filename and not os.path.isabs(filename): 498 | filename = __getFileName(filename) 499 | 500 | # use default if no filename given 501 | if not filename: 502 | candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" 503 | filename = __getFileName(candidate) 504 | 505 | if not filename: 506 | raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) 507 | 508 | # check if it is a .menu file 509 | if not os.path.splitext(filename)[1] == ".menu": 510 | raise ParsingError('Not a .menu file', filename) 511 | 512 | # create xml parser 513 | try: 514 | doc = xml.dom.minidom.parse(filename) 515 | except xml.parsers.expat.ExpatError: 516 | raise ParsingError('Not a valid .menu file', filename) 517 | 518 | # parse menufile 519 | tmp["Root"] = "" 520 | tmp["mergeFiles"] = [] 521 | tmp["DirectoryDirs"] = [] 522 | tmp["cache"] = MenuEntryCache() 523 | 524 | __parse(doc, filename, tmp["Root"]) 525 | __parsemove(tmp["Root"]) 526 | __postparse(tmp["Root"]) 527 | 528 | tmp["Root"].Doc = doc 529 | tmp["Root"].Filename = filename 530 | 531 | # generate the menu 532 | __genmenuNotOnlyAllocated(tmp["Root"]) 533 | __genmenuOnlyAllocated(tmp["Root"]) 534 | 535 | # and finally sort 536 | sort(tmp["Root"]) 537 | 538 | return tmp["Root"] 539 | 540 | 541 | def __parse(node, filename, parent=None): 542 | for child in node.childNodes: 543 | if child.nodeType == ELEMENT_NODE: 544 | if child.tagName == 'Menu': 545 | __parseMenu(child, filename, parent) 546 | elif child.tagName == 'AppDir': 547 | try: 548 | __parseAppDir(child.childNodes[0].nodeValue, filename, parent) 549 | except IndexError: 550 | raise ValidationError('AppDir cannot be empty', filename) 551 | elif child.tagName == 'DefaultAppDirs': 552 | __parseDefaultAppDir(filename, parent) 553 | elif child.tagName == 'DirectoryDir': 554 | try: 555 | __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent) 556 | except IndexError: 557 | raise ValidationError('DirectoryDir cannot be empty', filename) 558 | elif child.tagName == 'DefaultDirectoryDirs': 559 | __parseDefaultDirectoryDir(filename, parent) 560 | elif child.tagName == 'Name' : 561 | try: 562 | parent.Name = child.childNodes[0].nodeValue 563 | except IndexError: 564 | raise ValidationError('Name cannot be empty', filename) 565 | elif child.tagName == 'Directory' : 566 | try: 567 | parent.Directories.append(child.childNodes[0].nodeValue) 568 | except IndexError: 569 | raise ValidationError('Directory cannot be empty', filename) 570 | elif child.tagName == 'OnlyUnallocated': 571 | parent.OnlyUnallocated = True 572 | elif child.tagName == 'NotOnlyUnallocated': 573 | parent.OnlyUnallocated = False 574 | elif child.tagName == 'Deleted': 575 | parent.Deleted = True 576 | elif child.tagName == 'NotDeleted': 577 | parent.Deleted = False 578 | elif child.tagName == 'Include' or child.tagName == 'Exclude': 579 | parent.Rules.append(Rule(child.tagName, child)) 580 | elif child.tagName == 'MergeFile': 581 | try: 582 | if child.getAttribute("type") == "parent": 583 | __parseMergeFile("applications.menu", child, filename, parent) 584 | else: 585 | __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent) 586 | except IndexError: 587 | raise ValidationError('MergeFile cannot be empty', filename) 588 | elif child.tagName == 'MergeDir': 589 | try: 590 | __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent) 591 | except IndexError: 592 | raise ValidationError('MergeDir cannot be empty', filename) 593 | elif child.tagName == 'DefaultMergeDirs': 594 | __parseDefaultMergeDirs(child, filename, parent) 595 | elif child.tagName == 'Move': 596 | parent.Moves.append(Move(child)) 597 | elif child.tagName == 'Layout': 598 | if len(child.childNodes) > 1: 599 | parent.Layout = Layout(child) 600 | elif child.tagName == 'DefaultLayout': 601 | if len(child.childNodes) > 1: 602 | parent.DefaultLayout = Layout(child) 603 | elif child.tagName == 'LegacyDir': 604 | try: 605 | __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent) 606 | except IndexError: 607 | raise ValidationError('LegacyDir cannot be empty', filename) 608 | elif child.tagName == 'KDELegacyDirs': 609 | __parseKDELegacyDirs(filename, parent) 610 | 611 | def __parsemove(menu): 612 | for submenu in menu.Submenus: 613 | __parsemove(submenu) 614 | 615 | # parse move operations 616 | for move in menu.Moves: 617 | move_from_menu = menu.getMenu(move.Old) 618 | if move_from_menu: 619 | move_to_menu = menu.getMenu(move.New) 620 | 621 | menus = move.New.split("/") 622 | oldparent = None 623 | while len(menus) > 0: 624 | if not oldparent: 625 | oldparent = menu 626 | newmenu = oldparent.getMenu(menus[0]) 627 | if not newmenu: 628 | newmenu = Menu() 629 | newmenu.Name = menus[0] 630 | if len(menus) > 1: 631 | newmenu.NotInXml = True 632 | oldparent.addSubmenu(newmenu) 633 | oldparent = newmenu 634 | menus.pop(0) 635 | 636 | newmenu += move_from_menu 637 | move_from_menu.Parent.Submenus.remove(move_from_menu) 638 | 639 | def __postparse(menu): 640 | # unallocated / deleted 641 | if menu.Deleted == "notset": 642 | menu.Deleted = False 643 | if menu.OnlyUnallocated == "notset": 644 | menu.OnlyUnallocated = False 645 | 646 | # Layout Tags 647 | if not menu.Layout or not menu.DefaultLayout: 648 | if menu.DefaultLayout: 649 | menu.Layout = menu.DefaultLayout 650 | elif menu.Layout: 651 | if menu.Depth > 0: 652 | menu.DefaultLayout = menu.Parent.DefaultLayout 653 | else: 654 | menu.DefaultLayout = Layout() 655 | else: 656 | if menu.Depth > 0: 657 | menu.Layout = menu.Parent.DefaultLayout 658 | menu.DefaultLayout = menu.Parent.DefaultLayout 659 | else: 660 | menu.Layout = Layout() 661 | menu.DefaultLayout = Layout() 662 | 663 | # add parent's app/directory dirs 664 | if menu.Depth > 0: 665 | menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs 666 | menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs 667 | 668 | # remove duplicates 669 | menu.Directories = __removeDuplicates(menu.Directories) 670 | menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs) 671 | menu.AppDirs = __removeDuplicates(menu.AppDirs) 672 | 673 | # go recursive through all menus 674 | for submenu in menu.Submenus: 675 | __postparse(submenu) 676 | 677 | # reverse so handling is easier 678 | menu.Directories.reverse() 679 | menu.DirectoryDirs.reverse() 680 | menu.AppDirs.reverse() 681 | 682 | # get the valid .directory file out of the list 683 | for directory in menu.Directories: 684 | for dir in menu.DirectoryDirs: 685 | if os.path.isfile(os.path.join(dir, directory)): 686 | menuentry = MenuEntry(directory, dir) 687 | if not menu.Directory: 688 | menu.Directory = menuentry 689 | elif menuentry.getType() == "System": 690 | if menu.Directory.getType() == "User": 691 | menu.Directory.Original = menuentry 692 | if menu.Directory: 693 | break 694 | 695 | 696 | # Menu parsing stuff 697 | def __parseMenu(child, filename, parent): 698 | m = Menu() 699 | __parse(child, filename, m) 700 | if parent: 701 | parent.addSubmenu(m) 702 | else: 703 | tmp["Root"] = m 704 | 705 | # helper function 706 | def __check(value, filename, type): 707 | path = os.path.dirname(filename) 708 | 709 | if not os.path.isabs(value): 710 | value = os.path.join(path, value) 711 | 712 | value = os.path.abspath(value) 713 | 714 | if type == "dir" and os.path.exists(value) and os.path.isdir(value): 715 | return value 716 | elif type == "file" and os.path.exists(value) and os.path.isfile(value): 717 | return value 718 | else: 719 | return False 720 | 721 | # App/Directory Dir Stuff 722 | def __parseAppDir(value, filename, parent): 723 | value = __check(value, filename, "dir") 724 | if value: 725 | parent.AppDirs.append(value) 726 | 727 | def __parseDefaultAppDir(filename, parent): 728 | for dir in reversed(xdg_data_dirs): 729 | __parseAppDir(os.path.join(dir, "applications"), filename, parent) 730 | 731 | def __parseDirectoryDir(value, filename, parent): 732 | value = __check(value, filename, "dir") 733 | if value: 734 | parent.DirectoryDirs.append(value) 735 | 736 | def __parseDefaultDirectoryDir(filename, parent): 737 | for dir in reversed(xdg_data_dirs): 738 | __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent) 739 | 740 | # Merge Stuff 741 | def __parseMergeFile(value, child, filename, parent): 742 | if child.getAttribute("type") == "parent": 743 | for dir in xdg_config_dirs: 744 | rel_file = filename.replace(dir, "").strip("/") 745 | if rel_file != filename: 746 | for p in xdg_config_dirs: 747 | if dir == p: 748 | continue 749 | if os.path.isfile(os.path.join(p,rel_file)): 750 | __mergeFile(os.path.join(p,rel_file),child,parent) 751 | break 752 | else: 753 | value = __check(value, filename, "file") 754 | if value: 755 | __mergeFile(value, child, parent) 756 | 757 | def __parseMergeDir(value, child, filename, parent): 758 | value = __check(value, filename, "dir") 759 | if value: 760 | for item in os.listdir(value): 761 | try: 762 | if os.path.splitext(item)[1] == ".menu": 763 | __mergeFile(os.path.join(value, item), child, parent) 764 | except UnicodeDecodeError: 765 | continue 766 | 767 | def __parseDefaultMergeDirs(child, filename, parent): 768 | basename = os.path.splitext(os.path.basename(filename))[0] 769 | for dir in reversed(xdg_config_dirs): 770 | __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent) 771 | 772 | def __mergeFile(filename, child, parent): 773 | # check for infinite loops 774 | if filename in tmp["mergeFiles"]: 775 | if debug: 776 | raise ParsingError('Infinite MergeFile loop detected', filename) 777 | else: 778 | return 779 | 780 | tmp["mergeFiles"].append(filename) 781 | 782 | # load file 783 | try: 784 | doc = xml.dom.minidom.parse(filename) 785 | except IOError: 786 | if debug: 787 | raise ParsingError('File not found', filename) 788 | else: 789 | return 790 | except xml.parsers.expat.ExpatError: 791 | if debug: 792 | raise ParsingError('Not a valid .menu file', filename) 793 | else: 794 | return 795 | 796 | # append file 797 | for child in doc.childNodes: 798 | if child.nodeType == ELEMENT_NODE: 799 | __parse(child,filename,parent) 800 | break 801 | 802 | # Legacy Dir Stuff 803 | def __parseLegacyDir(dir, prefix, filename, parent): 804 | m = __mergeLegacyDir(dir,prefix,filename,parent) 805 | if m: 806 | parent += m 807 | 808 | def __mergeLegacyDir(dir, prefix, filename, parent): 809 | dir = __check(dir,filename,"dir") 810 | if dir and dir not in tmp["DirectoryDirs"]: 811 | tmp["DirectoryDirs"].append(dir) 812 | 813 | m = Menu() 814 | m.AppDirs.append(dir) 815 | m.DirectoryDirs.append(dir) 816 | m.Name = os.path.basename(dir) 817 | m.NotInXml = True 818 | 819 | for item in os.listdir(dir): 820 | try: 821 | if item == ".directory": 822 | m.Directories.append(item) 823 | elif os.path.isdir(os.path.join(dir,item)): 824 | m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent)) 825 | except UnicodeDecodeError: 826 | continue 827 | 828 | tmp["cache"].addMenuEntries([dir],prefix, True) 829 | menuentries = tmp["cache"].getMenuEntries([dir], False) 830 | 831 | for menuentry in menuentries: 832 | categories = menuentry.Categories 833 | if len(categories) == 0: 834 | r = Rule("Include") 835 | r.parseFilename(menuentry.DesktopFileID) 836 | r.compile() 837 | m.Rules.append(r) 838 | if not dir in parent.AppDirs: 839 | categories.append("Legacy") 840 | menuentry.Categories = categories 841 | 842 | return m 843 | 844 | def __parseKDELegacyDirs(filename, parent): 845 | f=os.popen3("kde-config --path apps") 846 | output = f[1].readlines() 847 | try: 848 | for dir in output[0].split(":"): 849 | __parseLegacyDir(dir,"kde", filename, parent) 850 | except IndexError: 851 | pass 852 | 853 | # remove duplicate entries from a list 854 | def __removeDuplicates(list): 855 | set = {} 856 | list.reverse() 857 | list = [set.setdefault(e,e) for e in list if e not in set] 858 | list.reverse() 859 | return list 860 | 861 | # Finally generate the menu 862 | def __genmenuNotOnlyAllocated(menu): 863 | for submenu in menu.Submenus: 864 | __genmenuNotOnlyAllocated(submenu) 865 | 866 | if menu.OnlyUnallocated == False: 867 | tmp["cache"].addMenuEntries(menu.AppDirs) 868 | menuentries = [] 869 | for rule in menu.Rules: 870 | menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1) 871 | for menuentry in menuentries: 872 | if menuentry.Add == True: 873 | menuentry.Parents.append(menu) 874 | menuentry.Add = False 875 | menuentry.Allocated = True 876 | menu.MenuEntries.append(menuentry) 877 | 878 | def __genmenuOnlyAllocated(menu): 879 | for submenu in menu.Submenus: 880 | __genmenuOnlyAllocated(submenu) 881 | 882 | if menu.OnlyUnallocated == True: 883 | tmp["cache"].addMenuEntries(menu.AppDirs) 884 | menuentries = [] 885 | for rule in menu.Rules: 886 | menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2) 887 | for menuentry in menuentries: 888 | if menuentry.Add == True: 889 | menuentry.Parents.append(menu) 890 | # menuentry.Add = False 891 | # menuentry.Allocated = True 892 | menu.MenuEntries.append(menuentry) 893 | 894 | # And sorting ... 895 | def sort(menu): 896 | menu.Entries = [] 897 | menu.Visible = 0 898 | 899 | for submenu in menu.Submenus: 900 | sort(submenu) 901 | 902 | tmp_s = [] 903 | tmp_e = [] 904 | 905 | for order in menu.Layout.order: 906 | if order[0] == "Filename": 907 | tmp_e.append(order[1]) 908 | elif order[0] == "Menuname": 909 | tmp_s.append(order[1]) 910 | 911 | for order in menu.Layout.order: 912 | if order[0] == "Separator": 913 | separator = Separator(menu) 914 | if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator): 915 | separator.Show = False 916 | menu.Entries.append(separator) 917 | elif order[0] == "Filename": 918 | menuentry = menu.getMenuEntry(order[1]) 919 | if menuentry: 920 | menu.Entries.append(menuentry) 921 | elif order[0] == "Menuname": 922 | submenu = menu.getMenu(order[1]) 923 | if submenu: 924 | __parse_inline(submenu, menu) 925 | elif order[0] == "Merge": 926 | if order[1] == "files" or order[1] == "all": 927 | menu.MenuEntries.sort() 928 | for menuentry in menu.MenuEntries: 929 | if menuentry not in tmp_e: 930 | menu.Entries.append(menuentry) 931 | elif order[1] == "menus" or order[1] == "all": 932 | menu.Submenus.sort() 933 | for submenu in menu.Submenus: 934 | if submenu.Name not in tmp_s: 935 | __parse_inline(submenu, menu) 936 | 937 | # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec 938 | for entry in menu.Entries: 939 | entry.Show = True 940 | menu.Visible += 1 941 | if isinstance(entry, Menu): 942 | if entry.Deleted == True: 943 | entry.Show = "Deleted" 944 | menu.Visible -= 1 945 | elif isinstance(entry.Directory, MenuEntry): 946 | if entry.Directory.DesktopEntry.getNoDisplay() == True: 947 | entry.Show = "NoDisplay" 948 | menu.Visible -= 1 949 | elif entry.Directory.DesktopEntry.getHidden() == True: 950 | entry.Show = "Hidden" 951 | menu.Visible -= 1 952 | elif isinstance(entry, MenuEntry): 953 | if entry.DesktopEntry.getNoDisplay() == True: 954 | entry.Show = "NoDisplay" 955 | menu.Visible -= 1 956 | elif entry.DesktopEntry.getHidden() == True: 957 | entry.Show = "Hidden" 958 | menu.Visible -= 1 959 | elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()): 960 | entry.Show = "NoExec" 961 | menu.Visible -= 1 962 | elif xdg.Config.windowmanager: 963 | if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \ 964 | or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn(): 965 | entry.Show = "NotShowIn" 966 | menu.Visible -= 1 967 | elif isinstance(entry,Separator): 968 | menu.Visible -= 1 969 | 970 | # remove separators at the beginning and at the end 971 | if len(menu.Entries) > 0: 972 | if isinstance(menu.Entries[0], Separator): 973 | menu.Entries[0].Show = False 974 | if len(menu.Entries) > 1: 975 | if isinstance(menu.Entries[-1], Separator): 976 | menu.Entries[-1].Show = False 977 | 978 | # show_empty tag 979 | for entry in menu.Entries: 980 | if isinstance(entry,Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0: 981 | entry.Show = "Empty" 982 | menu.Visible -= 1 983 | if entry.NotInXml == True: 984 | menu.Entries.remove(entry) 985 | 986 | def __try_exec(executable): 987 | paths = os.environ['PATH'].split(os.pathsep) 988 | if not os.path.isfile(executable): 989 | for p in paths: 990 | f = os.path.join(p, executable) 991 | if os.path.isfile(f): 992 | if os.access(f, os.X_OK): 993 | return True 994 | else: 995 | if os.access(executable, os.X_OK): 996 | return True 997 | return False 998 | 999 | # inline tags 1000 | def __parse_inline(submenu, menu): 1001 | if submenu.Layout.inline == "true": 1002 | if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true": 1003 | menuentry = submenu.Entries[0] 1004 | menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True) 1005 | menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True) 1006 | menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True) 1007 | menu.Entries.append(menuentry) 1008 | elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: 1009 | if submenu.Layout.inline_header == "true": 1010 | header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) 1011 | menu.Entries.append(header) 1012 | for entry in submenu.Entries: 1013 | menu.Entries.append(entry) 1014 | else: 1015 | menu.Entries.append(submenu) 1016 | else: 1017 | menu.Entries.append(submenu) 1018 | 1019 | class MenuEntryCache: 1020 | "Class to cache Desktop Entries" 1021 | def __init__(self): 1022 | self.cacheEntries = {} 1023 | self.cacheEntries['legacy'] = [] 1024 | self.cache = {} 1025 | 1026 | def addMenuEntries(self, dirs, prefix="", legacy=False): 1027 | for dir in dirs: 1028 | if not self.cacheEntries.has_key(dir): 1029 | self.cacheEntries[dir] = [] 1030 | self.__addFiles(dir, "", prefix, legacy) 1031 | 1032 | def __addFiles(self, dir, subdir, prefix, legacy): 1033 | for item in os.listdir(os.path.join(dir,subdir)): 1034 | if os.path.splitext(item)[1] == ".desktop": 1035 | try: 1036 | menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix) 1037 | except ParsingError: 1038 | continue 1039 | 1040 | self.cacheEntries[dir].append(menuentry) 1041 | if legacy == True: 1042 | self.cacheEntries['legacy'].append(menuentry) 1043 | elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False: 1044 | self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy) 1045 | 1046 | def getMenuEntries(self, dirs, legacy=True): 1047 | list = [] 1048 | ids = [] 1049 | # handle legacy items 1050 | appdirs = dirs[:] 1051 | if legacy == True: 1052 | appdirs.append("legacy") 1053 | # cache the results again 1054 | key = "".join(appdirs) 1055 | try: 1056 | return self.cache[key] 1057 | except KeyError: 1058 | pass 1059 | for dir in appdirs: 1060 | for menuentry in self.cacheEntries[dir]: 1061 | try: 1062 | if menuentry.DesktopFileID not in ids: 1063 | ids.append(menuentry.DesktopFileID) 1064 | list.append(menuentry) 1065 | elif menuentry.getType() == "System": 1066 | # FIXME: This is only 99% correct, but still... 1067 | i = list.index(menuentry) 1068 | e = list[i] 1069 | if e.getType() == "User": 1070 | e.Original = menuentry 1071 | except UnicodeDecodeError: 1072 | continue 1073 | self.cache[key] = list 1074 | return list 1075 | --------------------------------------------------------------------------------