瀏覽代碼

Merge pull request #2154 from enkore/merge/1.0-maint

cherry pick changes back from 1.0-maint (one-off)
enkore 8 年之前
父節點
當前提交
a659d1bf5f
共有 12 個文件被更改,包括 2911 次插入78 次删除
  1. 1 0
      AUTHORS
  2. 30 0
      docs/changes.rst
  3. 2 0
      docs/conf.py
  4. 4 1
      docs/quickstart.rst
  5. 3 1
      scripts/borg.exe.spec
  6. 7 1
      setup.py
  7. 147 53
      src/borg/archiver.py
  8. 90 14
      src/borg/helpers.py
  9. 16 4
      src/borg/keymanager.py
  10. 2441 0
      src/borg/paperkey.html
  11. 60 0
      src/borg/testsuite/archiver.py
  12. 110 4
      src/borg/testsuite/helpers.py

+ 1 - 0
AUTHORS

@@ -8,6 +8,7 @@ Borg authors ("The Borg Collective")
 - Michael Hanselmann <public@hansmi.ch>
 - Michael Hanselmann <public@hansmi.ch>
 - Teemu Toivanen <public@profnetti.fi>
 - Teemu Toivanen <public@profnetti.fi>
 - Marian Beermann <public@enkore.de>
 - Marian Beermann <public@enkore.de>
+- Martin Hostettler <textshell@uchuujin.de>
 - Daniel Reichelt <hacking@nachtgeist.net>
 - Daniel Reichelt <hacking@nachtgeist.net>
 - Lauri Niskanen <ape@ape3000.com>
 - Lauri Niskanen <ape@ape3000.com>
 
 

+ 30 - 0
docs/changes.rst

@@ -145,6 +145,36 @@ New features:
   --keep-exclude-tags, to account for the change mentioned above.
   --keep-exclude-tags, to account for the change mentioned above.
 
 
 
 
+Version 1.0.10 (2017-02-13)
+---------------------------
+
+Bug fixes:
+
+- Manifest timestamps are now monotonically increasing,
+  this fixes issues when the system clock jumps backwards
+  or is set inconsistently across computers accessing the same repository, #2115
+- Fixed testing regression in 1.0.10rc1 that lead to a hard dependency on
+  py.test >= 3.0, #2112
+
+New features:
+
+- "key export" can now generate a printable HTML page with both a QR code and
+  a human-readable "paperkey" representation (and custom text) through the
+  ``--qr-html`` option.
+
+  The same functionality is also available through `paperkey.html <paperkey.html>`_,
+  which is the same HTML page generated by ``--qr-html``. It works with existing
+  "key export" files and key files.
+
+Other changes:
+
+- docs:
+
+  - language clarification - "borg create --one-file-system" option does not respect
+    mount points, but considers different file systems instead, #2141
+- setup.py: build_api: sort file list for determinism
+
+
 Version 1.1.0b3 (2017-01-15)
 Version 1.1.0b3 (2017-01-15)
 ----------------------------
 ----------------------------
 
 

+ 2 - 0
docs/conf.py

@@ -140,6 +140,8 @@ html_favicon = '_static/favicon.ico'
 # so a file named "default.css" will overwrite the builtin "default.css".
 # so a file named "default.css" will overwrite the builtin "default.css".
 html_static_path = ['borg_theme']
 html_static_path = ['borg_theme']
 
 
+html_extra_path = ['../src/borg/paperkey.html']
+
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
 # using the given strftime format.
 html_last_updated_fmt = '%Y-%m-%d'
 html_last_updated_fmt = '%Y-%m-%d'

+ 4 - 1
docs/quickstart.rst

@@ -188,11 +188,14 @@ For automated backups the passphrase can be specified using the
     You can make backups using :ref:`borg_key_export` subcommand.
     You can make backups using :ref:`borg_key_export` subcommand.
 
 
     If you want to print a backup of your key to paper use the ``--paper``
     If you want to print a backup of your key to paper use the ``--paper``
-    option of this command and print the result.
+    option of this command and print the result, or this print `template`_
+    if you need a version with QR-Code.
 
 
     A backup inside of the backup that is encrypted with that key/passphrase
     A backup inside of the backup that is encrypted with that key/passphrase
     won't help you with that, of course.
     won't help you with that, of course.
 
 
+.. _template: paperkey.html
+
 .. _remote_repos:
 .. _remote_repos:
 
 
 Remote repositories
 Remote repositories

+ 3 - 1
scripts/borg.exe.spec

@@ -10,7 +10,9 @@ block_cipher = None
 a = Analysis([os.path.join(basepath, 'src/borg/__main__.py'), ],
 a = Analysis([os.path.join(basepath, 'src/borg/__main__.py'), ],
              pathex=[basepath, ],
              pathex=[basepath, ],
              binaries=[],
              binaries=[],
-             datas=[],
+             datas=[
+                 ('../src/borg/paperkey.html', 'borg'),
+             ],
              hiddenimports=['borg.platform.posix'],
              hiddenimports=['borg.platform.posix'],
              hookspath=[],
              hookspath=[],
              runtime_hooks=[],
              runtime_hooks=[],

+ 7 - 1
setup.py

@@ -584,10 +584,13 @@ class build_api(Command):
         print("auto-generating API documentation")
         print("auto-generating API documentation")
         with open("docs/api.rst", "w") as doc:
         with open("docs/api.rst", "w") as doc:
             doc.write("""
             doc.write("""
+.. IMPORTANT: this file is auto-generated by "setup.py build_api", do not edit!
+
+
 API Documentation
 API Documentation
 =================
 =================
 """)
 """)
-            for mod in glob('src/borg/*.py') + glob('src/borg/*.pyx'):
+            for mod in sorted(glob('src/borg/*.py') + glob('src/borg/*.pyx')):
                 print("examining module %s" % mod)
                 print("examining module %s" % mod)
                 mod = mod.replace('.pyx', '').replace('.py', '').replace('/', '.')
                 mod = mod.replace('.pyx', '').replace('.py', '').replace('/', '.')
                 if "._" not in mod:
                 if "._" not in mod:
@@ -666,6 +669,9 @@ setup(
             'borgfs = borg.archiver:main',
             'borgfs = borg.archiver:main',
         ]
         ]
     },
     },
+    package_data={
+        'borg': ['paperkey.html']
+    },
     cmdclass=cmdclass,
     cmdclass=cmdclass,
     ext_modules=ext_modules,
     ext_modules=ext_modules,
     setup_requires=['setuptools_scm>=1.7'],
     setup_requires=['setuptools_scm>=1.7'],

+ 147 - 53
src/borg/archiver.py

@@ -44,7 +44,8 @@ from .helpers import to_localtime, timestamp
 from .helpers import get_cache_dir
 from .helpers import get_cache_dir
 from .helpers import Manifest
 from .helpers import Manifest
 from .helpers import StableDict
 from .helpers import StableDict
-from .helpers import update_excludes, check_extension_modules
+from .helpers import check_extension_modules
+from .helpers import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
 from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo
 from .helpers import dir_is_tagged, is_slow_msgpack, yes, sysinfo
 from .helpers import log_multi
 from .helpers import log_multi
 from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern
 from .helpers import parse_pattern, PatternMatcher, PathPrefixPattern
@@ -128,7 +129,7 @@ class Archiver:
     def __init__(self, lock_wait=None, prog=None):
     def __init__(self, lock_wait=None, prog=None):
         self.exit_code = EXIT_SUCCESS
         self.exit_code = EXIT_SUCCESS
         self.lock_wait = lock_wait
         self.lock_wait = lock_wait
-        self.parser = self.build_parser(prog)
+        self.prog = prog
 
 
     def print_error(self, msg, *args):
     def print_error(self, msg, *args):
         msg = args and msg % args or msg
         msg = args and msg % args or msg
@@ -172,10 +173,10 @@ class Archiver:
             bi += slicelen
             bi += slicelen
 
 
     @staticmethod
     @staticmethod
-    def build_matcher(excludes, paths):
+    def build_matcher(inclexcl_patterns, paths):
         matcher = PatternMatcher()
         matcher = PatternMatcher()
-        if excludes:
-            matcher.add(excludes, False)
+        if inclexcl_patterns:
+            matcher.add_inclexcl(inclexcl_patterns)
         include_patterns = []
         include_patterns = []
         if paths:
         if paths:
             include_patterns.extend(parse_pattern(i, PathPrefixPattern) for i in paths)
             include_patterns.extend(parse_pattern(i, PathPrefixPattern) for i in paths)
@@ -271,7 +272,10 @@ class Archiver:
             if not args.path:
             if not args.path:
                 self.print_error("output file to export key to expected")
                 self.print_error("output file to export key to expected")
                 return EXIT_ERROR
                 return EXIT_ERROR
-            manager.export(args.path)
+            if args.qr:
+                manager.export_qr(args.path)
+            else:
+                manager.export(args.path)
         return EXIT_SUCCESS
         return EXIT_SUCCESS
 
 
     @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
     @with_repository(lock=False, exclusive=False, manifest=False, cache=False)
@@ -313,8 +317,7 @@ class Archiver:
     def do_create(self, args, repository, manifest=None, key=None):
     def do_create(self, args, repository, manifest=None, key=None):
         """Create new archive"""
         """Create new archive"""
         matcher = PatternMatcher(fallback=True)
         matcher = PatternMatcher(fallback=True)
-        if args.excludes:
-            matcher.add(args.excludes, False)
+        matcher.add_inclexcl(args.patterns)
 
 
         def create_inner(archive, cache):
         def create_inner(archive, cache):
             # Add cache dir to inode_skip list
             # Add cache dir to inode_skip list
@@ -520,7 +523,7 @@ class Archiver:
             if sys.platform.startswith(('linux', 'freebsd', 'netbsd', 'openbsd', 'darwin', )):
             if sys.platform.startswith(('linux', 'freebsd', 'netbsd', 'openbsd', 'darwin', )):
                 logger.warning('Hint: You likely need to fix your locale setup. E.g. install locales and use: LANG=en_US.UTF-8')
                 logger.warning('Hint: You likely need to fix your locale setup. E.g. install locales and use: LANG=en_US.UTF-8')
 
 
-        matcher, include_patterns = self.build_matcher(args.excludes, args.paths)
+        matcher, include_patterns = self.build_matcher(args.patterns, args.paths)
 
 
         progress = args.progress
         progress = args.progress
         output_list = args.output_list
         output_list = args.output_list
@@ -790,7 +793,7 @@ class Archiver:
                                'If you know for certain that they are the same, pass --same-chunker-params '
                                'If you know for certain that they are the same, pass --same-chunker-params '
                                'to override this check.')
                                'to override this check.')
 
 
-        matcher, include_patterns = self.build_matcher(args.excludes, args.paths)
+        matcher, include_patterns = self.build_matcher(args.patterns, args.paths)
 
 
         compare_archives(archive1, archive2, matcher)
         compare_archives(archive1, archive2, matcher)
 
 
@@ -924,7 +927,7 @@ class Archiver:
             return self._list_repository(args, manifest, write)
             return self._list_repository(args, manifest, write)
 
 
     def _list_archive(self, args, repository, manifest, key, write):
     def _list_archive(self, args, repository, manifest, key, write):
-        matcher, _ = self.build_matcher(args.excludes, args.paths)
+        matcher, _ = self.build_matcher(args.patterns, args.paths)
         with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache:
         with Cache(repository, key, manifest, lock_wait=self.lock_wait) as cache:
             archive = Archive(repository, key, manifest, args.location.archive, cache=cache,
             archive = Archive(repository, key, manifest, args.location.archive, cache=cache,
                               consider_part_files=args.consider_part_files)
                               consider_part_files=args.consider_part_files)
@@ -1154,7 +1157,7 @@ class Archiver:
                    env_var_override='BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING'):
                    env_var_override='BORG_RECREATE_I_KNOW_WHAT_I_AM_DOING'):
             return EXIT_ERROR
             return EXIT_ERROR
 
 
-        matcher, include_patterns = self.build_matcher(args.excludes, args.paths)
+        matcher, include_patterns = self.build_matcher(args.patterns, args.paths)
         self.output_list = args.output_list
         self.output_list = args.output_list
         self.output_filter = args.output_filter
         self.output_filter = args.output_filter
 
 
@@ -1398,8 +1401,9 @@ class Archiver:
 
 
     helptext = collections.OrderedDict()
     helptext = collections.OrderedDict()
     helptext['patterns'] = textwrap.dedent('''
     helptext['patterns'] = textwrap.dedent('''
-        Exclusion patterns support four separate styles, fnmatch, shell, regular
-        expressions and path prefixes. By default, fnmatch is used. If followed
+        File patterns support four separate styles: fnmatch, shell, regular
+        expressions and path prefixes. By default, fnmatch is used for
+        `--exclude` patterns and shell-style is used for `--pattern`. If followed
         by a colon (':') the first two characters of a pattern are used as a
         by a colon (':') the first two characters of a pattern are used as a
         style selector. Explicit style selection is necessary when a
         style selector. Explicit style selection is necessary when a
         non-default style is desired or when the desired pattern starts with
         non-default style is desired or when the desired pattern starts with
@@ -1407,12 +1411,12 @@ class Archiver:
 
 
         `Fnmatch <https://docs.python.org/3/library/fnmatch.html>`_, selector `fm:`
         `Fnmatch <https://docs.python.org/3/library/fnmatch.html>`_, selector `fm:`
 
 
-            This is the default style.  These patterns use a variant of shell
-            pattern syntax, with '*' matching any number of characters, '?'
-            matching any single character, '[...]' matching any single
-            character specified, including ranges, and '[!...]' matching any
-            character not specified. For the purpose of these patterns, the
-            path separator ('\\' for Windows and '/' on other systems) is not
+            This is the default style for --exclude and --exclude-from.
+            These patterns use a variant of shell pattern syntax, with '*' matching
+            any number of characters, '?' matching any single character, '[...]'
+            matching any single character specified, including ranges, and '[!...]'
+            matching any character not specified. For the purpose of these patterns,
+            the path separator ('\\' for Windows and '/' on other systems) is not
             treated specially. Wrap meta-characters in brackets for a literal
             treated specially. Wrap meta-characters in brackets for a literal
             match (i.e. `[?]` to match the literal character `?`). For a path
             match (i.e. `[?]` to match the literal character `?`). For a path
             to match a pattern, it must completely match from start to end, or
             to match a pattern, it must completely match from start to end, or
@@ -1423,6 +1427,7 @@ class Archiver:
 
 
         Shell-style patterns, selector `sh:`
         Shell-style patterns, selector `sh:`
 
 
+            This is the default style for --pattern and --patterns-from.
             Like fnmatch patterns these are similar to shell patterns. The difference
             Like fnmatch patterns these are similar to shell patterns. The difference
             is that the pattern may include `**/` for matching zero or more directory
             is that the pattern may include `**/` for matching zero or more directory
             levels, `*` for matching zero or more arbitrary characters with the
             levels, `*` for matching zero or more arbitrary characters with the
@@ -1483,7 +1488,39 @@ class Archiver:
             re:^/home/[^/]\.tmp/
             re:^/home/[^/]\.tmp/
             sh:/home/*/.thumbnails
             sh:/home/*/.thumbnails
             EOF
             EOF
-            $ borg create --exclude-from exclude.txt backup /\n\n''')
+            $ borg create --exclude-from exclude.txt backup /
+
+
+        A more general and easier to use way to define filename matching patterns exists
+        with the `--pattern` and `--patterns-from` options. Using these, you may specify
+        the backup roots (starting points) and patterns for inclusion/exclusion. A
+        root path starts with the prefix `R`, followed by a path (a plain path, not a
+        file pattern). An include rule starts with the prefix +, an exclude rule starts
+        with the prefix -, both followed by a pattern.
+        Inclusion patterns are useful to include pathes that are contained in an excluded
+        path. The first matching pattern is used so if an include pattern matches before
+        an exclude pattern, the file is backed up.
+
+        Note that the default pattern style for `--pattern` and `--patterns-from` is
+        shell style (`sh:`), so those patterns behave similar to rsync include/exclude
+        patterns.
+
+        Patterns (`--pattern`) and excludes (`--exclude`) from the command line are
+        considered first (in the order of appearance). Then patterns from `--patterns-from`
+        are added. Exclusion patterns from `--exclude-from` files are appended last.
+
+        An example `--patterns-from` file could look like that::
+
+            R /
+            # can be rebuild
+            - /home/*/.cache
+            # they're downloads for a reason
+            - /home/*/Downloads
+            # susan is a nice person
+            # include susans home
+            + /home/susan
+            # don't backup the other home directories
+            - /home/*\n\n''')
     helptext['placeholders'] = textwrap.dedent('''
     helptext['placeholders'] = textwrap.dedent('''
         Repository (or Archive) URLs, --prefix and --remote-path values support these
         Repository (or Archive) URLs, --prefix and --remote-path values support these
         placeholders:
         placeholders:
@@ -1714,6 +1751,9 @@ class Archiver:
                             help='show version number and exit')
                             help='show version number and exit')
         subparsers = parser.add_subparsers(title='required arguments', metavar='<command>')
         subparsers = parser.add_subparsers(title='required arguments', metavar='<command>')
 
 
+        # some empty defaults for all subparsers
+        common_parser.set_defaults(paths=[], patterns=[])
+
         serve_epilog = process_epilog("""
         serve_epilog = process_epilog("""
         This command starts a repository server process. This command is usually not used manually.
         This command starts a repository server process. This command is usually not used manually.
         """)
         """)
@@ -1938,6 +1978,9 @@ class Archiver:
         subparser.add_argument('--paper', dest='paper', action='store_true',
         subparser.add_argument('--paper', dest='paper', action='store_true',
                                default=False,
                                default=False,
                                help='Create an export suitable for printing and later type-in')
                                help='Create an export suitable for printing and later type-in')
+        subparser.add_argument('--qr-html', dest='qr', action='store_true',
+                               default=False,
+                               help='Create an html file suitable for printing and later type-in or qr scan')
 
 
         key_import_epilog = process_epilog("""
         key_import_epilog = process_epilog("""
         This command allows to restore a key previously backed up with the
         This command allows to restore a key previously backed up with the
@@ -2108,11 +2151,10 @@ class Archiver:
                                help='only display items with the given status characters')
                                help='only display items with the given status characters')
 
 
         exclude_group = subparser.add_argument_group('Exclusion options')
         exclude_group = subparser.add_argument_group('Exclusion options')
-        exclude_group.add_argument('-e', '--exclude', dest='excludes',
-                                   type=parse_pattern, action='append',
+        exclude_group.add_argument('-e', '--exclude', dest='patterns',
+                                   type=parse_exclude_pattern, action='append',
                                    metavar="PATTERN", help='exclude paths matching PATTERN')
                                    metavar="PATTERN", help='exclude paths matching PATTERN')
-        exclude_group.add_argument('--exclude-from', dest='exclude_files',
-                                   type=argparse.FileType('r'), action='append',
+        exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction,
                                    metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
                                    metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
         exclude_group.add_argument('--exclude-caches', dest='exclude_caches',
         exclude_group.add_argument('--exclude-caches', dest='exclude_caches',
                                    action='store_true', default=False,
                                    action='store_true', default=False,
@@ -2126,11 +2168,16 @@ class Archiver:
                                    action='store_true', default=False,
                                    action='store_true', default=False,
                                    help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise '
                                    help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise '
                                         'excluded caches/directories')
                                         'excluded caches/directories')
+        exclude_group.add_argument('--pattern',
+                                   action=ArgparsePatternAction,
+                                   metavar="PATTERN", help='include/exclude paths matching PATTERN')
+        exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction,
+                                   metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line')
 
 
         fs_group = subparser.add_argument_group('Filesystem options')
         fs_group = subparser.add_argument_group('Filesystem options')
         fs_group.add_argument('-x', '--one-file-system', dest='one_file_system',
         fs_group.add_argument('-x', '--one-file-system', dest='one_file_system',
                               action='store_true', default=False,
                               action='store_true', default=False,
-                              help='stay in same file system, do not cross mount points')
+                              help='stay in the same file system and do not store mount points of other file systems')
         fs_group.add_argument('--numeric-owner', dest='numeric_owner',
         fs_group.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               action='store_true', default=False,
                               help='only store numeric user and group identifiers')
                               help='only store numeric user and group identifiers')
@@ -2177,7 +2224,7 @@ class Archiver:
         subparser.add_argument('location', metavar='ARCHIVE',
         subparser.add_argument('location', metavar='ARCHIVE',
                                type=location_validator(archive=True),
                                type=location_validator(archive=True),
                                help='name of archive to create (must be also a valid directory name)')
                                help='name of archive to create (must be also a valid directory name)')
-        subparser.add_argument('paths', metavar='PATH', nargs='+', type=str,
+        subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                                help='paths to archive')
                                help='paths to archive')
 
 
         extract_epilog = process_epilog("""
         extract_epilog = process_epilog("""
@@ -2207,12 +2254,15 @@ class Archiver:
         subparser.add_argument('-n', '--dry-run', dest='dry_run',
         subparser.add_argument('-n', '--dry-run', dest='dry_run',
                                default=False, action='store_true',
                                default=False, action='store_true',
                                help='do not actually change any files')
                                help='do not actually change any files')
-        subparser.add_argument('-e', '--exclude', dest='excludes',
-                               type=parse_pattern, action='append',
+        subparser.add_argument('-e', '--exclude', dest='patterns',
+                               type=parse_exclude_pattern, action='append',
                                metavar="PATTERN", help='exclude paths matching PATTERN')
                                metavar="PATTERN", help='exclude paths matching PATTERN')
-        subparser.add_argument('--exclude-from', dest='exclude_files',
-                               type=argparse.FileType('r'), action='append',
+        subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction,
                                metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
                                metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
+        subparser.add_argument('--pattern', action=ArgparsePatternAction,
+                               metavar="PATTERN", help='include/exclude paths matching PATTERN')
+        subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction,
+                               metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line')
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
                                action='store_true', default=False,
                                action='store_true', default=False,
                                help='only obey numeric user and group identifiers')
                                help='only obey numeric user and group identifiers')
@@ -2255,12 +2305,6 @@ class Archiver:
                                           formatter_class=argparse.RawDescriptionHelpFormatter,
                                           formatter_class=argparse.RawDescriptionHelpFormatter,
                                           help='find differences in archive contents')
                                           help='find differences in archive contents')
         subparser.set_defaults(func=self.do_diff)
         subparser.set_defaults(func=self.do_diff)
-        subparser.add_argument('-e', '--exclude', dest='excludes',
-                               type=parse_pattern, action='append',
-                               metavar="PATTERN", help='exclude paths matching PATTERN')
-        subparser.add_argument('--exclude-from', dest='exclude_files',
-                               type=argparse.FileType('r'), action='append',
-                               metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
         subparser.add_argument('--numeric-owner', dest='numeric_owner',
                                action='store_true', default=False,
                                action='store_true', default=False,
                                help='only consider numeric user and group identifiers')
                                help='only consider numeric user and group identifiers')
@@ -2279,6 +2323,30 @@ class Archiver:
         subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
         subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                                help='paths of items inside the archives to compare; patterns are supported')
                                help='paths of items inside the archives to compare; patterns are supported')
 
 
+        exclude_group = subparser.add_argument_group('Exclusion options')
+        exclude_group.add_argument('-e', '--exclude', dest='patterns',
+                                   type=parse_exclude_pattern, action='append',
+                                   metavar="PATTERN", help='exclude paths matching PATTERN')
+        exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction,
+                                   metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
+        exclude_group.add_argument('--exclude-caches', dest='exclude_caches',
+                                   action='store_true', default=False,
+                                   help='exclude directories that contain a CACHEDIR.TAG file ('
+                                        'http://www.brynosaurus.com/cachedir/spec.html)')
+        exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present',
+                                   metavar='NAME', action='append', type=str,
+                                   help='exclude directories that are tagged by containing a filesystem object with '
+                                        'the given NAME')
+        exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags',
+                                   action='store_true', default=False,
+                                   help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise '
+                                        'excluded caches/directories')
+        exclude_group.add_argument('--pattern',
+                                   action=ArgparsePatternAction,
+                                   metavar="PATTERN", help='include/exclude paths matching PATTERN')
+        exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction,
+                                   metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line')
+
         rename_epilog = process_epilog("""
         rename_epilog = process_epilog("""
         This command renames an archive in the repository.
         This command renames an archive in the repository.
 
 
@@ -2359,12 +2427,6 @@ class Archiver:
         subparser.add_argument('--format', '--list-format', dest='format', type=str,
         subparser.add_argument('--format', '--list-format', dest='format', type=str,
                                help="""specify format for file listing
                                help="""specify format for file listing
                                 (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""")
                                 (default: "{mode} {user:6} {group:6} {size:8d} {isomtime} {path}{extra}{NL}")""")
-        subparser.add_argument('-e', '--exclude', dest='excludes',
-                               type=parse_pattern, action='append',
-                               metavar="PATTERN", help='exclude paths matching PATTERN')
-        subparser.add_argument('--exclude-from', dest='exclude_files',
-                               type=argparse.FileType('r'), action='append',
-                               metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
         subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='',
         subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='',
                                type=location_validator(),
                                type=location_validator(),
                                help='repository/archive to list contents of')
                                help='repository/archive to list contents of')
@@ -2372,6 +2434,30 @@ class Archiver:
                                help='paths to list; patterns are supported')
                                help='paths to list; patterns are supported')
         self.add_archives_filters_args(subparser)
         self.add_archives_filters_args(subparser)
 
 
+        exclude_group = subparser.add_argument_group('Exclusion options')
+        exclude_group.add_argument('-e', '--exclude', dest='patterns',
+                                   type=parse_exclude_pattern, action='append',
+                                   metavar="PATTERN", help='exclude paths matching PATTERN')
+        exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction,
+                                   metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
+        exclude_group.add_argument('--exclude-caches', dest='exclude_caches',
+                                   action='store_true', default=False,
+                                   help='exclude directories that contain a CACHEDIR.TAG file ('
+                                        'http://www.brynosaurus.com/cachedir/spec.html)')
+        exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present',
+                                   metavar='NAME', action='append', type=str,
+                                   help='exclude directories that are tagged by containing a filesystem object with '
+                                        'the given NAME')
+        exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags',
+                                   action='store_true', default=False,
+                                   help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise '
+                                        'excluded caches/directories')
+        exclude_group.add_argument('--pattern',
+                                   action=ArgparsePatternAction,
+                                   metavar="PATTERN", help='include/exclude paths matching PATTERN')
+        exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction,
+                                   metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line')
+
         mount_epilog = process_epilog("""
         mount_epilog = process_epilog("""
         This command mounts an archive as a FUSE filesystem. This can be useful for
         This command mounts an archive as a FUSE filesystem. This can be useful for
         browsing an archive or restoring individual files. Unless the ``--foreground``
         browsing an archive or restoring individual files. Unless the ``--foreground``
@@ -2712,11 +2798,10 @@ class Archiver:
                                help='print statistics at end')
                                help='print statistics at end')
 
 
         exclude_group = subparser.add_argument_group('Exclusion options')
         exclude_group = subparser.add_argument_group('Exclusion options')
-        exclude_group.add_argument('-e', '--exclude', dest='excludes',
-                                   type=parse_pattern, action='append',
+        exclude_group.add_argument('-e', '--exclude', dest='patterns',
+                                   type=parse_exclude_pattern, action='append',
                                    metavar="PATTERN", help='exclude paths matching PATTERN')
                                    metavar="PATTERN", help='exclude paths matching PATTERN')
-        exclude_group.add_argument('--exclude-from', dest='exclude_files',
-                                   type=argparse.FileType('r'), action='append',
+        exclude_group.add_argument('--exclude-from', action=ArgparseExcludeFileAction,
                                    metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
                                    metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
         exclude_group.add_argument('--exclude-caches', dest='exclude_caches',
         exclude_group.add_argument('--exclude-caches', dest='exclude_caches',
                                    action='store_true', default=False,
                                    action='store_true', default=False,
@@ -2724,12 +2809,17 @@ class Archiver:
                                         'http://www.brynosaurus.com/cachedir/spec.html)')
                                         'http://www.brynosaurus.com/cachedir/spec.html)')
         exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present',
         exclude_group.add_argument('--exclude-if-present', dest='exclude_if_present',
                                    metavar='NAME', action='append', type=str,
                                    metavar='NAME', action='append', type=str,
-                                   help='exclude directories that are tagged by containing a filesystem object with \
-                                         the given NAME')
+                                   help='exclude directories that are tagged by containing a filesystem object with '
+                                        'the given NAME')
         exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags',
         exclude_group.add_argument('--keep-exclude-tags', '--keep-tag-files', dest='keep_exclude_tags',
                                    action='store_true', default=False,
                                    action='store_true', default=False,
-                                   help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise \
-                                         excluded caches/directories')
+                                   help='keep tag objects (i.e.: arguments to --exclude-if-present) in otherwise '
+                                        'excluded caches/directories')
+        exclude_group.add_argument('--pattern',
+                                   action=ArgparsePatternAction,
+                                   metavar="PATTERN", help='include/exclude paths matching PATTERN')
+        exclude_group.add_argument('--patterns-from', action=ArgparsePatternFileAction,
+                                   metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line')
 
 
         archive_group = subparser.add_argument_group('Archive options')
         archive_group = subparser.add_argument_group('Archive options')
         archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None,
         archive_group.add_argument('--target', dest='target', metavar='TARGET', default=None,
@@ -2992,8 +3082,12 @@ class Archiver:
         # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
         # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
         if args:
         if args:
             args = self.preprocess_args(args)
             args = self.preprocess_args(args)
-        args = self.parser.parse_args(args or ['-h'])
-        update_excludes(args)
+        parser = self.build_parser(self.prog)
+        args = parser.parse_args(args or ['-h'])
+        if args.func == self.do_create:
+            # need at least 1 path but args.paths may also be populated from patterns
+            if not args.paths:
+                parser.error('Need at least one PATH argument.')
         return args
         return args
 
 
     def prerun_checks(self, logger):
     def prerun_checks(self, logger):

+ 90 - 14
src/borg/helpers.py

@@ -200,6 +200,7 @@ class Manifest:
         self.repository = repository
         self.repository = repository
         self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS
         self.item_keys = frozenset(item_keys) if item_keys is not None else ITEM_KEYS
         self.tam_verified = False
         self.tam_verified = False
+        self.timestamp = None
 
 
     @property
     @property
     def id_str(self):
     def id_str(self):
@@ -245,7 +246,13 @@ class Manifest:
         from .item import ManifestItem
         from .item import ManifestItem
         if self.key.tam_required:
         if self.key.tam_required:
             self.config[b'tam_required'] = True
             self.config[b'tam_required'] = True
-        self.timestamp = datetime.utcnow().isoformat()
+        # self.timestamp needs to be strictly monotonically increasing. Clocks often are not set correctly
+        if self.timestamp is None:
+            self.timestamp = datetime.utcnow().isoformat()
+        else:
+            prev_ts = datetime.strptime(self.timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+            incremented = (prev_ts + timedelta(microseconds=1)).isoformat()
+            self.timestamp = max(incremented, datetime.utcnow().isoformat())
         manifest = ManifestItem(
         manifest = ManifestItem(
             version=1,
             version=1,
             archives=StableDict(self.archives.get_raw_dict()),
             archives=StableDict(self.archives.get_raw_dict()),
@@ -355,21 +362,52 @@ def parse_timestamp(timestamp):
         return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
         return datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
 
 
 
 
-def load_excludes(fh):
-    """Load and parse exclude patterns from file object. Lines empty or starting with '#' after stripping whitespace on
-    both line ends are ignored.
-    """
-    return [parse_pattern(pattern) for pattern in clean_lines(fh)]
+def parse_add_pattern(patternstr, roots, patterns):
+    """Parse a pattern string and add it to roots or patterns depending on the pattern type."""
+    pattern = parse_inclexcl_pattern(patternstr)
+    if pattern.ptype is RootPath:
+        roots.append(pattern.pattern)
+    else:
+        patterns.append(pattern)
+
+
+def load_pattern_file(fileobj, roots, patterns):
+    for patternstr in clean_lines(fileobj):
+        parse_add_pattern(patternstr, roots, patterns)
+
+
+def load_exclude_file(fileobj, patterns):
+    for patternstr in clean_lines(fileobj):
+        patterns.append(parse_exclude_pattern(patternstr))
+
+
+class ArgparsePatternAction(argparse.Action):
+    def __init__(self, nargs=1, **kw):
+        super().__init__(nargs=nargs, **kw)
+
+    def __call__(self, parser, args, values, option_string=None):
+        parse_add_pattern(values[0], args.paths, args.patterns)
+
+
+class ArgparsePatternFileAction(argparse.Action):
+    def __init__(self, nargs=1, **kw):
+        super().__init__(nargs=nargs, **kw)
+
+    def __call__(self, parser, args, values, option_string=None):
+        """Load and parse patterns from a file.
+        Lines empty or starting with '#' after stripping whitespace on both line ends are ignored.
+        """
+        filename = values[0]
+        with open(filename) as f:
+            self.parse(f, args)
 
 
+    def parse(self, fobj, args):
+        load_pattern_file(fobj, args.roots, args.patterns)
 
 
-def update_excludes(args):
-    """Merge exclude patterns from files with those on command line."""
-    if hasattr(args, 'exclude_files') and args.exclude_files:
-        if not hasattr(args, 'excludes') or args.excludes is None:
-            args.excludes = []
-        for file in args.exclude_files:
-            args.excludes += load_excludes(file)
-            file.close()
+
+class ArgparseExcludeFileAction(ArgparsePatternFileAction):
+    def parse(self, fobj, args):
+        load_exclude_file(fobj, args.patterns)
 
 
 
 
 class PatternMatcher:
 class PatternMatcher:
@@ -388,6 +426,12 @@ class PatternMatcher:
         """
         """
         self._items.extend((i, value) for i in patterns)
         self._items.extend((i, value) for i in patterns)
 
 
+    def add_inclexcl(self, patterns):
+        """Add list of patterns (of type InclExclPattern) to internal list. The patterns ptype member is returned from
+        the match function when one of the given patterns matches.
+        """
+        self._items.extend(patterns)
+
     def match(self, path):
     def match(self, path):
         for (pattern, value) in self._items:
         for (pattern, value) in self._items:
             if pattern.match(path):
             if pattern.match(path):
@@ -539,6 +583,9 @@ _PATTERN_STYLES = set([
 
 
 _PATTERN_STYLE_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_STYLES)
 _PATTERN_STYLE_BY_PREFIX = dict((i.PREFIX, i) for i in _PATTERN_STYLES)
 
 
+InclExclPattern = namedtuple('InclExclPattern', 'pattern ptype')
+RootPath = object()
+
 
 
 def parse_pattern(pattern, fallback=FnmatchPattern):
 def parse_pattern(pattern, fallback=FnmatchPattern):
     """Read pattern from string and return an instance of the appropriate implementation class.
     """Read pattern from string and return an instance of the appropriate implementation class.
@@ -556,6 +603,35 @@ def parse_pattern(pattern, fallback=FnmatchPattern):
     return cls(pattern)
     return cls(pattern)
 
 
 
 
+def parse_exclude_pattern(pattern, fallback=FnmatchPattern):
+    """Read pattern from string and return an instance of the appropriate implementation class.
+    """
+    epattern = parse_pattern(pattern, fallback)
+    return InclExclPattern(epattern, False)
+
+
+def parse_inclexcl_pattern(pattern, fallback=ShellPattern):
+    """Read pattern from string and return a InclExclPattern object."""
+    type_prefix_map = {
+        '-': False,
+        '+': True,
+        'R': RootPath,
+        'r': RootPath,
+    }
+    try:
+        ptype = type_prefix_map[pattern[0]]
+        pattern = pattern[1:].lstrip()
+        if not pattern:
+            raise ValueError("Missing pattern!")
+    except (IndexError, KeyError, ValueError):
+        raise argparse.ArgumentTypeError("Unable to parse pattern: {}".format(pattern))
+    if ptype is RootPath:
+        pobj = pattern
+    else:
+        pobj = parse_pattern(pattern, fallback)
+    return InclExclPattern(pobj, ptype)
+
+
 def timestamp(s):
 def timestamp(s):
     """Convert a --timestamp=s argument to a datetime object"""
     """Convert a --timestamp=s argument to a datetime object"""
     try:
     try:

+ 16 - 4
src/borg/keymanager.py

@@ -2,6 +2,7 @@ from binascii import unhexlify, a2b_base64, b2a_base64
 import binascii
 import binascii
 import textwrap
 import textwrap
 from hashlib import sha256
 from hashlib import sha256
+import pkgutil
 
 
 from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey
 from .key import KeyfileKey, RepoKey, PassphraseKey, KeyfileNotFoundError, PlaintextKey
 from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex
 from .helpers import Manifest, NoManifestError, Error, yes, bin_to_hex
@@ -77,16 +78,27 @@ class KeyManager:
         elif self.keyblob_storage == KEYBLOB_REPO:
         elif self.keyblob_storage == KEYBLOB_REPO:
             self.repository.save_key(self.keyblob.encode('utf-8'))
             self.repository.save_key(self.keyblob.encode('utf-8'))
 
 
+    def get_keyfile_data(self):
+        data = '%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id))
+        data += self.keyblob
+        if not self.keyblob.endswith('\n'):
+            data += '\n'
+        return data
+
     def store_keyfile(self, target):
     def store_keyfile(self, target):
         with open(target, 'w') as fd:
         with open(target, 'w') as fd:
-            fd.write('%s %s\n' % (KeyfileKey.FILE_ID, bin_to_hex(self.repository.id)))
-            fd.write(self.keyblob)
-            if not self.keyblob.endswith('\n'):
-                fd.write('\n')
+            fd.write(self.get_keyfile_data())
 
 
     def export(self, path):
     def export(self, path):
         self.store_keyfile(path)
         self.store_keyfile(path)
 
 
+    def export_qr(self, path):
+        with open(path, 'wb') as fd:
+            key_data = self.get_keyfile_data()
+            html = pkgutil.get_data('borg', 'paperkey.html')
+            html = html.replace(b'</textarea>', key_data.encode() + b'</textarea>')
+            fd.write(html)
+
     def export_paperkey(self, path):
     def export_paperkey(self, path):
         def grouped(s):
         def grouped(s):
             ret = ''
             ret = ''

+ 2441 - 0
src/borg/paperkey.html

@@ -0,0 +1,2441 @@
+<!doctype html>
+<html moznomarginboxes mozdisallowselectionprint>
+<!--
+    Notes:
+        This may never cause external network connections. Everything needs to included in this file.
+        No minified libraries. Everything needs to be auditable and in preferred form of modification.
+
+        This file includes two libraries inline:
+            Kazuhiko Arase's qrcode-generator library (unpatched)
+            Chris Veness's sha256 implementation (locally modified to utf8Encode)
+        Both are MIT licensed.
+        As this script doesn’t interact with any untrusted parties / components it should be safe to
+        use local embedded copies of these libraries.
+-->
+
+
+<head>
+<title>BorgBackup Printable Key Template</title>
+<meta http-equiv="Content-Type" content="text/html; charset=Utf-8">
+<script>
+// https://github.com/kazuhikoarase/qrcode-generator/blob/master/js/qrcode.js
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+//  http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+//  http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = function() {
+
+  //---------------------------------------------------------------------
+  // qrcode
+  //---------------------------------------------------------------------
+
+  /**
+   * qrcode
+   * @param typeNumber 1 to 40
+   * @param errorCorrectionLevel 'L','M','Q','H'
+   */
+  var qrcode = function(typeNumber, errorCorrectionLevel) {
+
+    var PAD0 = 0xEC;
+    var PAD1 = 0x11;
+
+    var _typeNumber = typeNumber;
+    var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
+    var _modules = null;
+    var _moduleCount = 0;
+    var _dataCache = null;
+    var _dataList = new Array();
+
+    var _this = {};
+
+    var makeImpl = function(test, maskPattern) {
+
+      _moduleCount = _typeNumber * 4 + 17;
+      _modules = function(moduleCount) {
+        var modules = new Array(moduleCount);
+        for (var row = 0; row < moduleCount; row += 1) {
+          modules[row] = new Array(moduleCount);
+          for (var col = 0; col < moduleCount; col += 1) {
+            modules[row][col] = null;
+          }
+        }
+        return modules;
+      }(_moduleCount);
+
+      setupPositionProbePattern(0, 0);
+      setupPositionProbePattern(_moduleCount - 7, 0);
+      setupPositionProbePattern(0, _moduleCount - 7);
+      setupPositionAdjustPattern();
+      setupTimingPattern();
+      setupTypeInfo(test, maskPattern);
+
+      if (_typeNumber >= 7) {
+        setupTypeNumber(test);
+      }
+
+      if (_dataCache == null) {
+        _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
+      }
+
+      mapData(_dataCache, maskPattern);
+    };
+
+    var setupPositionProbePattern = function(row, col) {
+
+      for (var r = -1; r <= 7; r += 1) {
+
+        if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+        for (var c = -1; c <= 7; c += 1) {
+
+          if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+          if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
+              || (0 <= c && c <= 6 && (r == 0 || r == 6) )
+              || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
+            _modules[row + r][col + c] = true;
+          } else {
+            _modules[row + r][col + c] = false;
+          }
+        }
+      }
+    };
+
+    var getBestMaskPattern = function() {
+
+      var minLostPoint = 0;
+      var pattern = 0;
+
+      for (var i = 0; i < 8; i += 1) {
+
+        makeImpl(true, i);
+
+        var lostPoint = QRUtil.getLostPoint(_this);
+
+        if (i == 0 || minLostPoint > lostPoint) {
+          minLostPoint = lostPoint;
+          pattern = i;
+        }
+      }
+
+      return pattern;
+    };
+
+    var setupTimingPattern = function() {
+
+      for (var r = 8; r < _moduleCount - 8; r += 1) {
+        if (_modules[r][6] != null) {
+          continue;
+        }
+        _modules[r][6] = (r % 2 == 0);
+      }
+
+      for (var c = 8; c < _moduleCount - 8; c += 1) {
+        if (_modules[6][c] != null) {
+          continue;
+        }
+        _modules[6][c] = (c % 2 == 0);
+      }
+    };
+
+    var setupPositionAdjustPattern = function() {
+
+      var pos = QRUtil.getPatternPosition(_typeNumber);
+
+      for (var i = 0; i < pos.length; i += 1) {
+
+        for (var j = 0; j < pos.length; j += 1) {
+
+          var row = pos[i];
+          var col = pos[j];
+
+          if (_modules[row][col] != null) {
+            continue;
+          }
+
+          for (var r = -2; r <= 2; r += 1) {
+
+            for (var c = -2; c <= 2; c += 1) {
+
+              if (r == -2 || r == 2 || c == -2 || c == 2
+                  || (r == 0 && c == 0) ) {
+                _modules[row + r][col + c] = true;
+              } else {
+                _modules[row + r][col + c] = false;
+              }
+            }
+          }
+        }
+      }
+    };
+
+    var setupTypeNumber = function(test) {
+
+      var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+      for (var i = 0; i < 18; i += 1) {
+        var mod = (!test && ( (bits >> i) & 1) == 1);
+        _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
+      }
+
+      for (var i = 0; i < 18; i += 1) {
+        var mod = (!test && ( (bits >> i) & 1) == 1);
+        _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+      }
+    };
+
+    var setupTypeInfo = function(test, maskPattern) {
+
+      var data = (_errorCorrectionLevel << 3) | maskPattern;
+      var bits = QRUtil.getBCHTypeInfo(data);
+
+      // vertical
+      for (var i = 0; i < 15; i += 1) {
+
+        var mod = (!test && ( (bits >> i) & 1) == 1);
+
+        if (i < 6) {
+          _modules[i][8] = mod;
+        } else if (i < 8) {
+          _modules[i + 1][8] = mod;
+        } else {
+          _modules[_moduleCount - 15 + i][8] = mod;
+        }
+      }
+
+      // horizontal
+      for (var i = 0; i < 15; i += 1) {
+
+        var mod = (!test && ( (bits >> i) & 1) == 1);
+
+        if (i < 8) {
+          _modules[8][_moduleCount - i - 1] = mod;
+        } else if (i < 9) {
+          _modules[8][15 - i - 1 + 1] = mod;
+        } else {
+          _modules[8][15 - i - 1] = mod;
+        }
+      }
+
+      // fixed module
+      _modules[_moduleCount - 8][8] = (!test);
+    };
+
+    var mapData = function(data, maskPattern) {
+
+      var inc = -1;
+      var row = _moduleCount - 1;
+      var bitIndex = 7;
+      var byteIndex = 0;
+      var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+      for (var col = _moduleCount - 1; col > 0; col -= 2) {
+
+        if (col == 6) col -= 1;
+
+        while (true) {
+
+          for (var c = 0; c < 2; c += 1) {
+
+            if (_modules[row][col - c] == null) {
+
+              var dark = false;
+
+              if (byteIndex < data.length) {
+                dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
+              }
+
+              var mask = maskFunc(row, col - c);
+
+              if (mask) {
+                dark = !dark;
+              }
+
+              _modules[row][col - c] = dark;
+              bitIndex -= 1;
+
+              if (bitIndex == -1) {
+                byteIndex += 1;
+                bitIndex = 7;
+              }
+            }
+          }
+
+          row += inc;
+
+          if (row < 0 || _moduleCount <= row) {
+            row -= inc;
+            inc = -inc;
+            break;
+          }
+        }
+      }
+    };
+
+    var createBytes = function(buffer, rsBlocks) {
+
+      var offset = 0;
+
+      var maxDcCount = 0;
+      var maxEcCount = 0;
+
+      var dcdata = new Array(rsBlocks.length);
+      var ecdata = new Array(rsBlocks.length);
+
+      for (var r = 0; r < rsBlocks.length; r += 1) {
+
+        var dcCount = rsBlocks[r].dataCount;
+        var ecCount = rsBlocks[r].totalCount - dcCount;
+
+        maxDcCount = Math.max(maxDcCount, dcCount);
+        maxEcCount = Math.max(maxEcCount, ecCount);
+
+        dcdata[r] = new Array(dcCount);
+
+        for (var i = 0; i < dcdata[r].length; i += 1) {
+          dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+        }
+        offset += dcCount;
+
+        var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+        var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+        var modPoly = rawPoly.mod(rsPoly);
+        ecdata[r] = new Array(rsPoly.getLength() - 1);
+        for (var i = 0; i < ecdata[r].length; i += 1) {
+          var modIndex = i + modPoly.getLength() - ecdata[r].length;
+          ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
+        }
+      }
+
+      var totalCodeCount = 0;
+      for (var i = 0; i < rsBlocks.length; i += 1) {
+        totalCodeCount += rsBlocks[i].totalCount;
+      }
+
+      var data = new Array(totalCodeCount);
+      var index = 0;
+
+      for (var i = 0; i < maxDcCount; i += 1) {
+        for (var r = 0; r < rsBlocks.length; r += 1) {
+          if (i < dcdata[r].length) {
+            data[index] = dcdata[r][i];
+            index += 1;
+          }
+        }
+      }
+
+      for (var i = 0; i < maxEcCount; i += 1) {
+        for (var r = 0; r < rsBlocks.length; r += 1) {
+          if (i < ecdata[r].length) {
+            data[index] = ecdata[r][i];
+            index += 1;
+          }
+        }
+      }
+
+      return data;
+    };
+
+    var createData = function(typeNumber, errorCorrectionLevel, dataList) {
+
+      var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
+
+      var buffer = qrBitBuffer();
+
+      for (var i = 0; i < dataList.length; i += 1) {
+        var data = dataList[i];
+        buffer.put(data.getMode(), 4);
+        buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+        data.write(buffer);
+      }
+
+      // calc num max data.
+      var totalDataCount = 0;
+      for (var i = 0; i < rsBlocks.length; i += 1) {
+        totalDataCount += rsBlocks[i].dataCount;
+      }
+
+      if (buffer.getLengthInBits() > totalDataCount * 8) {
+        throw new Error('code length overflow. ('
+          + buffer.getLengthInBits()
+          + '>'
+          + totalDataCount * 8
+          + ')');
+      }
+
+      // end code
+      if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+        buffer.put(0, 4);
+      }
+
+      // padding
+      while (buffer.getLengthInBits() % 8 != 0) {
+        buffer.putBit(false);
+      }
+
+      // padding
+      while (true) {
+
+        if (buffer.getLengthInBits() >= totalDataCount * 8) {
+          break;
+        }
+        buffer.put(PAD0, 8);
+
+        if (buffer.getLengthInBits() >= totalDataCount * 8) {
+          break;
+        }
+        buffer.put(PAD1, 8);
+      }
+
+      return createBytes(buffer, rsBlocks);
+    };
+
+    _this.addData = function(data) {
+      var newData = qr8BitByte(data);
+      _dataList.push(newData);
+      _dataCache = null;
+    };
+
+    _this.isDark = function(row, col) {
+      if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+        throw new Error(row + ',' + col);
+      }
+      return _modules[row][col];
+    };
+
+    _this.getModuleCount = function() {
+      return _moduleCount;
+    };
+
+    _this.make = function() {
+      makeImpl(false, getBestMaskPattern() );
+    };
+
+    _this.createTableTag = function(cellSize, margin) {
+
+      cellSize = cellSize || 2;
+      margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+      var qrHtml = '';
+
+      qrHtml += '<table style="';
+      qrHtml += ' border-width: 0px; border-style: none;';
+      qrHtml += ' border-collapse: collapse;';
+      qrHtml += ' padding: 0px; margin: ' + margin + 'px;';
+      qrHtml += '">';
+      qrHtml += '<tbody>';
+
+      for (var r = 0; r < _this.getModuleCount(); r += 1) {
+
+        qrHtml += '<tr>';
+
+        for (var c = 0; c < _this.getModuleCount(); c += 1) {
+          qrHtml += '<td style="';
+          qrHtml += ' border-width: 0px; border-style: none;';
+          qrHtml += ' border-collapse: collapse;';
+          qrHtml += ' padding: 0px; margin: 0px;';
+          qrHtml += ' width: ' + cellSize + 'px;';
+          qrHtml += ' height: ' + cellSize + 'px;';
+          qrHtml += ' background-color: ';
+          qrHtml += _this.isDark(r, c)? '#000000' : '#ffffff';
+          qrHtml += ';';
+          qrHtml += '"/>';
+        }
+
+        qrHtml += '</tr>';
+      }
+
+      qrHtml += '</tbody>';
+      qrHtml += '</table>';
+
+      return qrHtml;
+    };
+
+    _this.createSvgTag = function(cellSize, margin) {
+
+      cellSize = cellSize || 2;
+      margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+      var size = _this.getModuleCount() * cellSize + margin * 2;
+      var c, mc, r, mr, qrSvg='', rect;
+
+      rect = 'l' + cellSize + ',0 0,' + cellSize +
+        ' -' + cellSize + ',0 0,-' + cellSize + 'z ';
+
+      qrSvg += '<svg';
+      qrSvg += ' width="' + size + 'px"';
+      qrSvg += ' height="' + size + 'px"';
+      qrSvg += ' xmlns="http://www.w3.org/2000/svg"';
+      qrSvg += '>';
+      qrSvg += '<path d="';
+
+      for (r = 0; r < _this.getModuleCount(); r += 1) {
+        mr = r * cellSize + margin;
+        for (c = 0; c < _this.getModuleCount(); c += 1) {
+          if (_this.isDark(r, c) ) {
+            mc = c*cellSize+margin;
+            qrSvg += 'M' + mc + ',' + mr + rect;
+          }
+        }
+      }
+
+      qrSvg += '" stroke="transparent" fill="black"/>';
+      qrSvg += '</svg>';
+
+      return qrSvg;
+    };
+
+    _this.createImgTag = function(cellSize, margin) {
+
+      cellSize = cellSize || 2;
+      margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+      var size = _this.getModuleCount() * cellSize + margin * 2;
+      var min = margin;
+      var max = size - margin;
+
+      return createImgTag(size, size, function(x, y) {
+        if (min <= x && x < max && min <= y && y < max) {
+          var c = Math.floor( (x - min) / cellSize);
+          var r = Math.floor( (y - min) / cellSize);
+          return _this.isDark(r, c)? 0 : 1;
+        } else {
+          return 1;
+        }
+      } );
+    };
+
+    return _this;
+  };
+
+  //---------------------------------------------------------------------
+  // qrcode.stringToBytes
+  //---------------------------------------------------------------------
+
+  qrcode.stringToBytes = function(s) {
+    var bytes = new Array();
+    for (var i = 0; i < s.length; i += 1) {
+      var c = s.charCodeAt(i);
+      bytes.push(c & 0xff);
+    }
+    return bytes;
+  };
+
+  //---------------------------------------------------------------------
+  // qrcode.createStringToBytes
+  //---------------------------------------------------------------------
+
+  /**
+   * @param unicodeData base64 string of byte array.
+   * [16bit Unicode],[16bit Bytes], ...
+   * @param numChars
+   */
+  qrcode.createStringToBytes = function(unicodeData, numChars) {
+
+    // create conversion map.
+
+    var unicodeMap = function() {
+
+      var bin = base64DecodeInputStream(unicodeData);
+      var read = function() {
+        var b = bin.read();
+        if (b == -1) throw new Error();
+        return b;
+      };
+
+      var count = 0;
+      var unicodeMap = {};
+      while (true) {
+        var b0 = bin.read();
+        if (b0 == -1) break;
+        var b1 = read();
+        var b2 = read();
+        var b3 = read();
+        var k = String.fromCharCode( (b0 << 8) | b1);
+        var v = (b2 << 8) | b3;
+        unicodeMap[k] = v;
+        count += 1;
+      }
+      if (count != numChars) {
+        throw new Error(count + ' != ' + numChars);
+      }
+
+      return unicodeMap;
+    }();
+
+    var unknownChar = '?'.charCodeAt(0);
+
+    return function(s) {
+      var bytes = new Array();
+      for (var i = 0; i < s.length; i += 1) {
+        var c = s.charCodeAt(i);
+        if (c < 128) {
+          bytes.push(c);
+        } else {
+          var b = unicodeMap[s.charAt(i)];
+          if (typeof b == 'number') {
+            if ( (b & 0xff) == b) {
+              // 1byte
+              bytes.push(b);
+            } else {
+              // 2bytes
+              bytes.push(b >>> 8);
+              bytes.push(b & 0xff);
+            }
+          } else {
+            bytes.push(unknownChar);
+          }
+        }
+      }
+      return bytes;
+    };
+  };
+
+  //---------------------------------------------------------------------
+  // QRMode
+  //---------------------------------------------------------------------
+
+  var QRMode = {
+    MODE_NUMBER :    1 << 0,
+    MODE_ALPHA_NUM : 1 << 1,
+    MODE_8BIT_BYTE : 1 << 2,
+    MODE_KANJI :     1 << 3
+  };
+
+  //---------------------------------------------------------------------
+  // QRErrorCorrectionLevel
+  //---------------------------------------------------------------------
+
+  var QRErrorCorrectionLevel = {
+    L : 1,
+    M : 0,
+    Q : 3,
+    H : 2
+  };
+
+  //---------------------------------------------------------------------
+  // QRMaskPattern
+  //---------------------------------------------------------------------
+
+  var QRMaskPattern = {
+    PATTERN000 : 0,
+    PATTERN001 : 1,
+    PATTERN010 : 2,
+    PATTERN011 : 3,
+    PATTERN100 : 4,
+    PATTERN101 : 5,
+    PATTERN110 : 6,
+    PATTERN111 : 7
+  };
+
+  //---------------------------------------------------------------------
+  // QRUtil
+  //---------------------------------------------------------------------
+
+  var QRUtil = function() {
+
+    var PATTERN_POSITION_TABLE = [
+      [],
+      [6, 18],
+      [6, 22],
+      [6, 26],
+      [6, 30],
+      [6, 34],
+      [6, 22, 38],
+      [6, 24, 42],
+      [6, 26, 46],
+      [6, 28, 50],
+      [6, 30, 54],
+      [6, 32, 58],
+      [6, 34, 62],
+      [6, 26, 46, 66],
+      [6, 26, 48, 70],
+      [6, 26, 50, 74],
+      [6, 30, 54, 78],
+      [6, 30, 56, 82],
+      [6, 30, 58, 86],
+      [6, 34, 62, 90],
+      [6, 28, 50, 72, 94],
+      [6, 26, 50, 74, 98],
+      [6, 30, 54, 78, 102],
+      [6, 28, 54, 80, 106],
+      [6, 32, 58, 84, 110],
+      [6, 30, 58, 86, 114],
+      [6, 34, 62, 90, 118],
+      [6, 26, 50, 74, 98, 122],
+      [6, 30, 54, 78, 102, 126],
+      [6, 26, 52, 78, 104, 130],
+      [6, 30, 56, 82, 108, 134],
+      [6, 34, 60, 86, 112, 138],
+      [6, 30, 58, 86, 114, 142],
+      [6, 34, 62, 90, 118, 146],
+      [6, 30, 54, 78, 102, 126, 150],
+      [6, 24, 50, 76, 102, 128, 154],
+      [6, 28, 54, 80, 106, 132, 158],
+      [6, 32, 58, 84, 110, 136, 162],
+      [6, 26, 54, 82, 110, 138, 166],
+      [6, 30, 58, 86, 114, 142, 170]
+    ];
+    var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);
+    var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);
+    var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
+
+    var _this = {};
+
+    var getBCHDigit = function(data) {
+      var digit = 0;
+      while (data != 0) {
+        digit += 1;
+        data >>>= 1;
+      }
+      return digit;
+    };
+
+    _this.getBCHTypeInfo = function(data) {
+      var d = data << 10;
+      while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
+        d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) );
+      }
+      return ( (data << 10) | d) ^ G15_MASK;
+    };
+
+    _this.getBCHTypeNumber = function(data) {
+      var d = data << 12;
+      while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
+        d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) );
+      }
+      return (data << 12) | d;
+    };
+
+    _this.getPatternPosition = function(typeNumber) {
+      return PATTERN_POSITION_TABLE[typeNumber - 1];
+    };
+
+    _this.getMaskFunction = function(maskPattern) {
+
+      switch (maskPattern) {
+
+      case QRMaskPattern.PATTERN000 :
+        return function(i, j) { return (i + j) % 2 == 0; };
+      case QRMaskPattern.PATTERN001 :
+        return function(i, j) { return i % 2 == 0; };
+      case QRMaskPattern.PATTERN010 :
+        return function(i, j) { return j % 3 == 0; };
+      case QRMaskPattern.PATTERN011 :
+        return function(i, j) { return (i + j) % 3 == 0; };
+      case QRMaskPattern.PATTERN100 :
+        return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; };
+      case QRMaskPattern.PATTERN101 :
+        return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };
+      case QRMaskPattern.PATTERN110 :
+        return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; };
+      case QRMaskPattern.PATTERN111 :
+        return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; };
+
+      default :
+        throw new Error('bad maskPattern:' + maskPattern);
+      }
+    };
+
+    _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
+      var a = qrPolynomial([1], 0);
+      for (var i = 0; i < errorCorrectLength; i += 1) {
+        a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) );
+      }
+      return a;
+    };
+
+    _this.getLengthInBits = function(mode, type) {
+
+      if (1 <= type && type < 10) {
+
+        // 1 - 9
+
+        switch(mode) {
+        case QRMode.MODE_NUMBER    : return 10;
+        case QRMode.MODE_ALPHA_NUM : return 9;
+        case QRMode.MODE_8BIT_BYTE : return 8;
+        case QRMode.MODE_KANJI     : return 8;
+        default :
+          throw new Error('mode:' + mode);
+        }
+
+      } else if (type < 27) {
+
+        // 10 - 26
+
+        switch(mode) {
+        case QRMode.MODE_NUMBER    : return 12;
+        case QRMode.MODE_ALPHA_NUM : return 11;
+        case QRMode.MODE_8BIT_BYTE : return 16;
+        case QRMode.MODE_KANJI     : return 10;
+        default :
+          throw new Error('mode:' + mode);
+        }
+
+      } else if (type < 41) {
+
+        // 27 - 40
+
+        switch(mode) {
+        case QRMode.MODE_NUMBER    : return 14;
+        case QRMode.MODE_ALPHA_NUM : return 13;
+        case QRMode.MODE_8BIT_BYTE : return 16;
+        case QRMode.MODE_KANJI     : return 12;
+        default :
+          throw new Error('mode:' + mode);
+        }
+
+      } else {
+        throw new Error('type:' + type);
+      }
+    };
+
+    _this.getLostPoint = function(qrcode) {
+
+      var moduleCount = qrcode.getModuleCount();
+
+      var lostPoint = 0;
+
+      // LEVEL1
+
+      for (var row = 0; row < moduleCount; row += 1) {
+        for (var col = 0; col < moduleCount; col += 1) {
+
+          var sameCount = 0;
+          var dark = qrcode.isDark(row, col);
+
+          for (var r = -1; r <= 1; r += 1) {
+
+            if (row + r < 0 || moduleCount <= row + r) {
+              continue;
+            }
+
+            for (var c = -1; c <= 1; c += 1) {
+
+              if (col + c < 0 || moduleCount <= col + c) {
+                continue;
+              }
+
+              if (r == 0 && c == 0) {
+                continue;
+              }
+
+              if (dark == qrcode.isDark(row + r, col + c) ) {
+                sameCount += 1;
+              }
+            }
+          }
+
+          if (sameCount > 5) {
+            lostPoint += (3 + sameCount - 5);
+          }
+        }
+      };
+
+      // LEVEL2
+
+      for (var row = 0; row < moduleCount - 1; row += 1) {
+        for (var col = 0; col < moduleCount - 1; col += 1) {
+          var count = 0;
+          if (qrcode.isDark(row, col) ) count += 1;
+          if (qrcode.isDark(row + 1, col) ) count += 1;
+          if (qrcode.isDark(row, col + 1) ) count += 1;
+          if (qrcode.isDark(row + 1, col + 1) ) count += 1;
+          if (count == 0 || count == 4) {
+            lostPoint += 3;
+          }
+        }
+      }
+
+      // LEVEL3
+
+      for (var row = 0; row < moduleCount; row += 1) {
+        for (var col = 0; col < moduleCount - 6; col += 1) {
+          if (qrcode.isDark(row, col)
+              && !qrcode.isDark(row, col + 1)
+              &&  qrcode.isDark(row, col + 2)
+              &&  qrcode.isDark(row, col + 3)
+              &&  qrcode.isDark(row, col + 4)
+              && !qrcode.isDark(row, col + 5)
+              &&  qrcode.isDark(row, col + 6) ) {
+            lostPoint += 40;
+          }
+        }
+      }
+
+      for (var col = 0; col < moduleCount; col += 1) {
+        for (var row = 0; row < moduleCount - 6; row += 1) {
+          if (qrcode.isDark(row, col)
+              && !qrcode.isDark(row + 1, col)
+              &&  qrcode.isDark(row + 2, col)
+              &&  qrcode.isDark(row + 3, col)
+              &&  qrcode.isDark(row + 4, col)
+              && !qrcode.isDark(row + 5, col)
+              &&  qrcode.isDark(row + 6, col) ) {
+            lostPoint += 40;
+          }
+        }
+      }
+
+      // LEVEL4
+
+      var darkCount = 0;
+
+      for (var col = 0; col < moduleCount; col += 1) {
+        for (var row = 0; row < moduleCount; row += 1) {
+          if (qrcode.isDark(row, col) ) {
+            darkCount += 1;
+          }
+        }
+      }
+
+      var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
+      lostPoint += ratio * 10;
+
+      return lostPoint;
+    };
+
+    return _this;
+  }();
+
+  //---------------------------------------------------------------------
+  // QRMath
+  //---------------------------------------------------------------------
+
+  var QRMath = function() {
+
+    var EXP_TABLE = new Array(256);
+    var LOG_TABLE = new Array(256);
+
+    // initialize tables
+    for (var i = 0; i < 8; i += 1) {
+      EXP_TABLE[i] = 1 << i;
+    }
+    for (var i = 8; i < 256; i += 1) {
+      EXP_TABLE[i] = EXP_TABLE[i - 4]
+        ^ EXP_TABLE[i - 5]
+        ^ EXP_TABLE[i - 6]
+        ^ EXP_TABLE[i - 8];
+    }
+    for (var i = 0; i < 255; i += 1) {
+      LOG_TABLE[EXP_TABLE[i] ] = i;
+    }
+
+    var _this = {};
+
+    _this.glog = function(n) {
+
+      if (n < 1) {
+        throw new Error('glog(' + n + ')');
+      }
+
+      return LOG_TABLE[n];
+    };
+
+    _this.gexp = function(n) {
+
+      while (n < 0) {
+        n += 255;
+      }
+
+      while (n >= 256) {
+        n -= 255;
+      }
+
+      return EXP_TABLE[n];
+    };
+
+    return _this;
+  }();
+
+  //---------------------------------------------------------------------
+  // qrPolynomial
+  //---------------------------------------------------------------------
+
+  function qrPolynomial(num, shift) {
+
+    if (typeof num.length == 'undefined') {
+      throw new Error(num.length + '/' + shift);
+    }
+
+    var _num = function() {
+      var offset = 0;
+      while (offset < num.length && num[offset] == 0) {
+        offset += 1;
+      }
+      var _num = new Array(num.length - offset + shift);
+      for (var i = 0; i < num.length - offset; i += 1) {
+        _num[i] = num[i + offset];
+      }
+      return _num;
+    }();
+
+    var _this = {};
+
+    _this.getAt = function(index) {
+      return _num[index];
+    };
+
+    _this.getLength = function() {
+      return _num.length;
+    };
+
+    _this.multiply = function(e) {
+
+      var num = new Array(_this.getLength() + e.getLength() - 1);
+
+      for (var i = 0; i < _this.getLength(); i += 1) {
+        for (var j = 0; j < e.getLength(); j += 1) {
+          num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) );
+        }
+      }
+
+      return qrPolynomial(num, 0);
+    };
+
+    _this.mod = function(e) {
+
+      if (_this.getLength() - e.getLength() < 0) {
+        return _this;
+      }
+
+      var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) );
+
+      var num = new Array(_this.getLength() );
+      for (var i = 0; i < _this.getLength(); i += 1) {
+        num[i] = _this.getAt(i);
+      }
+
+      for (var i = 0; i < e.getLength(); i += 1) {
+        num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio);
+      }
+
+      // recursive call
+      return qrPolynomial(num, 0).mod(e);
+    };
+
+    return _this;
+  };
+
+  //---------------------------------------------------------------------
+  // QRRSBlock
+  //---------------------------------------------------------------------
+
+  var QRRSBlock = function() {
+
+    var RS_BLOCK_TABLE = [
+
+      // L
+      // M
+      // Q
+      // H
+
+      // 1
+      [1, 26, 19],
+      [1, 26, 16],
+      [1, 26, 13],
+      [1, 26, 9],
+
+      // 2
+      [1, 44, 34],
+      [1, 44, 28],
+      [1, 44, 22],
+      [1, 44, 16],
+
+      // 3
+      [1, 70, 55],
+      [1, 70, 44],
+      [2, 35, 17],
+      [2, 35, 13],
+
+      // 4
+      [1, 100, 80],
+      [2, 50, 32],
+      [2, 50, 24],
+      [4, 25, 9],
+
+      // 5
+      [1, 134, 108],
+      [2, 67, 43],
+      [2, 33, 15, 2, 34, 16],
+      [2, 33, 11, 2, 34, 12],
+
+      // 6
+      [2, 86, 68],
+      [4, 43, 27],
+      [4, 43, 19],
+      [4, 43, 15],
+
+      // 7
+      [2, 98, 78],
+      [4, 49, 31],
+      [2, 32, 14, 4, 33, 15],
+      [4, 39, 13, 1, 40, 14],
+
+      // 8
+      [2, 121, 97],
+      [2, 60, 38, 2, 61, 39],
+      [4, 40, 18, 2, 41, 19],
+      [4, 40, 14, 2, 41, 15],
+
+      // 9
+      [2, 146, 116],
+      [3, 58, 36, 2, 59, 37],
+      [4, 36, 16, 4, 37, 17],
+      [4, 36, 12, 4, 37, 13],
+
+      // 10
+      [2, 86, 68, 2, 87, 69],
+      [4, 69, 43, 1, 70, 44],
+      [6, 43, 19, 2, 44, 20],
+      [6, 43, 15, 2, 44, 16],
+
+      // 11
+      [4, 101, 81],
+      [1, 80, 50, 4, 81, 51],
+      [4, 50, 22, 4, 51, 23],
+      [3, 36, 12, 8, 37, 13],
+
+      // 12
+      [2, 116, 92, 2, 117, 93],
+      [6, 58, 36, 2, 59, 37],
+      [4, 46, 20, 6, 47, 21],
+      [7, 42, 14, 4, 43, 15],
+
+      // 13
+      [4, 133, 107],
+      [8, 59, 37, 1, 60, 38],
+      [8, 44, 20, 4, 45, 21],
+      [12, 33, 11, 4, 34, 12],
+
+      // 14
+      [3, 145, 115, 1, 146, 116],
+      [4, 64, 40, 5, 65, 41],
+      [11, 36, 16, 5, 37, 17],
+      [11, 36, 12, 5, 37, 13],
+
+      // 15
+      [5, 109, 87, 1, 110, 88],
+      [5, 65, 41, 5, 66, 42],
+      [5, 54, 24, 7, 55, 25],
+      [11, 36, 12, 7, 37, 13],
+
+      // 16
+      [5, 122, 98, 1, 123, 99],
+      [7, 73, 45, 3, 74, 46],
+      [15, 43, 19, 2, 44, 20],
+      [3, 45, 15, 13, 46, 16],
+
+      // 17
+      [1, 135, 107, 5, 136, 108],
+      [10, 74, 46, 1, 75, 47],
+      [1, 50, 22, 15, 51, 23],
+      [2, 42, 14, 17, 43, 15],
+
+      // 18
+      [5, 150, 120, 1, 151, 121],
+      [9, 69, 43, 4, 70, 44],
+      [17, 50, 22, 1, 51, 23],
+      [2, 42, 14, 19, 43, 15],
+
+      // 19
+      [3, 141, 113, 4, 142, 114],
+      [3, 70, 44, 11, 71, 45],
+      [17, 47, 21, 4, 48, 22],
+      [9, 39, 13, 16, 40, 14],
+
+      // 20
+      [3, 135, 107, 5, 136, 108],
+      [3, 67, 41, 13, 68, 42],
+      [15, 54, 24, 5, 55, 25],
+      [15, 43, 15, 10, 44, 16],
+
+      // 21
+      [4, 144, 116, 4, 145, 117],
+      [17, 68, 42],
+      [17, 50, 22, 6, 51, 23],
+      [19, 46, 16, 6, 47, 17],
+
+      // 22
+      [2, 139, 111, 7, 140, 112],
+      [17, 74, 46],
+      [7, 54, 24, 16, 55, 25],
+      [34, 37, 13],
+
+      // 23
+      [4, 151, 121, 5, 152, 122],
+      [4, 75, 47, 14, 76, 48],
+      [11, 54, 24, 14, 55, 25],
+      [16, 45, 15, 14, 46, 16],
+
+      // 24
+      [6, 147, 117, 4, 148, 118],
+      [6, 73, 45, 14, 74, 46],
+      [11, 54, 24, 16, 55, 25],
+      [30, 46, 16, 2, 47, 17],
+
+      // 25
+      [8, 132, 106, 4, 133, 107],
+      [8, 75, 47, 13, 76, 48],
+      [7, 54, 24, 22, 55, 25],
+      [22, 45, 15, 13, 46, 16],
+
+      // 26
+      [10, 142, 114, 2, 143, 115],
+      [19, 74, 46, 4, 75, 47],
+      [28, 50, 22, 6, 51, 23],
+      [33, 46, 16, 4, 47, 17],
+
+      // 27
+      [8, 152, 122, 4, 153, 123],
+      [22, 73, 45, 3, 74, 46],
+      [8, 53, 23, 26, 54, 24],
+      [12, 45, 15, 28, 46, 16],
+
+      // 28
+      [3, 147, 117, 10, 148, 118],
+      [3, 73, 45, 23, 74, 46],
+      [4, 54, 24, 31, 55, 25],
+      [11, 45, 15, 31, 46, 16],
+
+      // 29
+      [7, 146, 116, 7, 147, 117],
+      [21, 73, 45, 7, 74, 46],
+      [1, 53, 23, 37, 54, 24],
+      [19, 45, 15, 26, 46, 16],
+
+      // 30
+      [5, 145, 115, 10, 146, 116],
+      [19, 75, 47, 10, 76, 48],
+      [15, 54, 24, 25, 55, 25],
+      [23, 45, 15, 25, 46, 16],
+
+      // 31
+      [13, 145, 115, 3, 146, 116],
+      [2, 74, 46, 29, 75, 47],
+      [42, 54, 24, 1, 55, 25],
+      [23, 45, 15, 28, 46, 16],
+
+      // 32
+      [17, 145, 115],
+      [10, 74, 46, 23, 75, 47],
+      [10, 54, 24, 35, 55, 25],
+      [19, 45, 15, 35, 46, 16],
+
+      // 33
+      [17, 145, 115, 1, 146, 116],
+      [14, 74, 46, 21, 75, 47],
+      [29, 54, 24, 19, 55, 25],
+      [11, 45, 15, 46, 46, 16],
+
+      // 34
+      [13, 145, 115, 6, 146, 116],
+      [14, 74, 46, 23, 75, 47],
+      [44, 54, 24, 7, 55, 25],
+      [59, 46, 16, 1, 47, 17],
+
+      // 35
+      [12, 151, 121, 7, 152, 122],
+      [12, 75, 47, 26, 76, 48],
+      [39, 54, 24, 14, 55, 25],
+      [22, 45, 15, 41, 46, 16],
+
+      // 36
+      [6, 151, 121, 14, 152, 122],
+      [6, 75, 47, 34, 76, 48],
+      [46, 54, 24, 10, 55, 25],
+      [2, 45, 15, 64, 46, 16],
+
+      // 37
+      [17, 152, 122, 4, 153, 123],
+      [29, 74, 46, 14, 75, 47],
+      [49, 54, 24, 10, 55, 25],
+      [24, 45, 15, 46, 46, 16],
+
+      // 38
+      [4, 152, 122, 18, 153, 123],
+      [13, 74, 46, 32, 75, 47],
+      [48, 54, 24, 14, 55, 25],
+      [42, 45, 15, 32, 46, 16],
+
+      // 39
+      [20, 147, 117, 4, 148, 118],
+      [40, 75, 47, 7, 76, 48],
+      [43, 54, 24, 22, 55, 25],
+      [10, 45, 15, 67, 46, 16],
+
+      // 40
+      [19, 148, 118, 6, 149, 119],
+      [18, 75, 47, 31, 76, 48],
+      [34, 54, 24, 34, 55, 25],
+      [20, 45, 15, 61, 46, 16]
+    ];
+
+    var qrRSBlock = function(totalCount, dataCount) {
+      var _this = {};
+      _this.totalCount = totalCount;
+      _this.dataCount = dataCount;
+      return _this;
+    };
+
+    var _this = {};
+
+    var getRsBlockTable = function(typeNumber, errorCorrectionLevel) {
+
+      switch(errorCorrectionLevel) {
+      case QRErrorCorrectionLevel.L :
+        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+      case QRErrorCorrectionLevel.M :
+        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+      case QRErrorCorrectionLevel.Q :
+        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+      case QRErrorCorrectionLevel.H :
+        return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+      default :
+        return undefined;
+      }
+    };
+
+    _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {
+
+      var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);
+
+      if (typeof rsBlock == 'undefined') {
+        throw new Error('bad rs block @ typeNumber:' + typeNumber +
+            '/errorCorrectionLevel:' + errorCorrectionLevel);
+      }
+
+      var length = rsBlock.length / 3;
+
+      var list = new Array();
+
+      for (var i = 0; i < length; i += 1) {
+
+        var count = rsBlock[i * 3 + 0];
+        var totalCount = rsBlock[i * 3 + 1];
+        var dataCount = rsBlock[i * 3 + 2];
+
+        for (var j = 0; j < count; j += 1) {
+          list.push(qrRSBlock(totalCount, dataCount) );
+        }
+      }
+
+      return list;
+    };
+
+    return _this;
+  }();
+
+  //---------------------------------------------------------------------
+  // qrBitBuffer
+  //---------------------------------------------------------------------
+
+  var qrBitBuffer = function() {
+
+    var _buffer = new Array();
+    var _length = 0;
+
+    var _this = {};
+
+    _this.getBuffer = function() {
+      return _buffer;
+    };
+
+    _this.getAt = function(index) {
+      var bufIndex = Math.floor(index / 8);
+      return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1;
+    };
+
+    _this.put = function(num, length) {
+      for (var i = 0; i < length; i += 1) {
+        _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1);
+      }
+    };
+
+    _this.getLengthInBits = function() {
+      return _length;
+    };
+
+    _this.putBit = function(bit) {
+
+      var bufIndex = Math.floor(_length / 8);
+      if (_buffer.length <= bufIndex) {
+        _buffer.push(0);
+      }
+
+      if (bit) {
+        _buffer[bufIndex] |= (0x80 >>> (_length % 8) );
+      }
+
+      _length += 1;
+    };
+
+    return _this;
+  };
+
+  //---------------------------------------------------------------------
+  // qr8BitByte
+  //---------------------------------------------------------------------
+
+  var qr8BitByte = function(data) {
+
+    var _mode = QRMode.MODE_8BIT_BYTE;
+    var _data = data;
+    var _bytes = qrcode.stringToBytes(data);
+
+    var _this = {};
+
+    _this.getMode = function() {
+      return _mode;
+    };
+
+    _this.getLength = function(buffer) {
+      return _bytes.length;
+    };
+
+    _this.write = function(buffer) {
+      for (var i = 0; i < _bytes.length; i += 1) {
+        buffer.put(_bytes[i], 8);
+      }
+    };
+
+    return _this;
+  };
+
+  //=====================================================================
+  // GIF Support etc.
+  //
+
+  //---------------------------------------------------------------------
+  // byteArrayOutputStream
+  //---------------------------------------------------------------------
+
+  var byteArrayOutputStream = function() {
+
+    var _bytes = new Array();
+
+    var _this = {};
+
+    _this.writeByte = function(b) {
+      _bytes.push(b & 0xff);
+    };
+
+    _this.writeShort = function(i) {
+      _this.writeByte(i);
+      _this.writeByte(i >>> 8);
+    };
+
+    _this.writeBytes = function(b, off, len) {
+      off = off || 0;
+      len = len || b.length;
+      for (var i = 0; i < len; i += 1) {
+        _this.writeByte(b[i + off]);
+      }
+    };
+
+    _this.writeString = function(s) {
+      for (var i = 0; i < s.length; i += 1) {
+        _this.writeByte(s.charCodeAt(i) );
+      }
+    };
+
+    _this.toByteArray = function() {
+      return _bytes;
+    };
+
+    _this.toString = function() {
+      var s = '';
+      s += '[';
+      for (var i = 0; i < _bytes.length; i += 1) {
+        if (i > 0) {
+          s += ',';
+        }
+        s += _bytes[i];
+      }
+      s += ']';
+      return s;
+    };
+
+    return _this;
+  };
+
+  //---------------------------------------------------------------------
+  // base64EncodeOutputStream
+  //---------------------------------------------------------------------
+
+  var base64EncodeOutputStream = function() {
+
+    var _buffer = 0;
+    var _buflen = 0;
+    var _length = 0;
+    var _base64 = '';
+
+    var _this = {};
+
+    var writeEncoded = function(b) {
+      _base64 += String.fromCharCode(encode(b & 0x3f) );
+    };
+
+    var encode = function(n) {
+      if (n < 0) {
+        // error.
+      } else if (n < 26) {
+        return 0x41 + n;
+      } else if (n < 52) {
+        return 0x61 + (n - 26);
+      } else if (n < 62) {
+        return 0x30 + (n - 52);
+      } else if (n == 62) {
+        return 0x2b;
+      } else if (n == 63) {
+        return 0x2f;
+      }
+      throw new Error('n:' + n);
+    };
+
+    _this.writeByte = function(n) {
+
+      _buffer = (_buffer << 8) | (n & 0xff);
+      _buflen += 8;
+      _length += 1;
+
+      while (_buflen >= 6) {
+        writeEncoded(_buffer >>> (_buflen - 6) );
+        _buflen -= 6;
+      }
+    };
+
+    _this.flush = function() {
+
+      if (_buflen > 0) {
+        writeEncoded(_buffer << (6 - _buflen) );
+        _buffer = 0;
+        _buflen = 0;
+      }
+
+      if (_length % 3 != 0) {
+        // padding
+        var padlen = 3 - _length % 3;
+        for (var i = 0; i < padlen; i += 1) {
+          _base64 += '=';
+        }
+      }
+    };
+
+    _this.toString = function() {
+      return _base64;
+    };
+
+    return _this;
+  };
+
+  //---------------------------------------------------------------------
+  // base64DecodeInputStream
+  //---------------------------------------------------------------------
+
+  var base64DecodeInputStream = function(str) {
+
+    var _str = str;
+    var _pos = 0;
+    var _buffer = 0;
+    var _buflen = 0;
+
+    var _this = {};
+
+    _this.read = function() {
+
+      while (_buflen < 8) {
+
+        if (_pos >= _str.length) {
+          if (_buflen == 0) {
+            return -1;
+          }
+          throw new Error('unexpected end of file./' + _buflen);
+        }
+
+        var c = _str.charAt(_pos);
+        _pos += 1;
+
+        if (c == '=') {
+          _buflen = 0;
+          return -1;
+        } else if (c.match(/^\s$/) ) {
+          // ignore if whitespace.
+          continue;
+        }
+
+        _buffer = (_buffer << 6) | decode(c.charCodeAt(0) );
+        _buflen += 6;
+      }
+
+      var n = (_buffer >>> (_buflen - 8) ) & 0xff;
+      _buflen -= 8;
+      return n;
+    };
+
+    var decode = function(c) {
+      if (0x41 <= c && c <= 0x5a) {
+        return c - 0x41;
+      } else if (0x61 <= c && c <= 0x7a) {
+        return c - 0x61 + 26;
+      } else if (0x30 <= c && c <= 0x39) {
+        return c - 0x30 + 52;
+      } else if (c == 0x2b) {
+        return 62;
+      } else if (c == 0x2f) {
+        return 63;
+      } else {
+        throw new Error('c:' + c);
+      }
+    };
+
+    return _this;
+  };
+
+  //---------------------------------------------------------------------
+  // gifImage (B/W)
+  //---------------------------------------------------------------------
+
+  var gifImage = function(width, height) {
+
+    var _width = width;
+    var _height = height;
+    var _data = new Array(width * height);
+
+    var _this = {};
+
+    _this.setPixel = function(x, y, pixel) {
+      _data[y * _width + x] = pixel;
+    };
+
+    _this.write = function(out) {
+
+      //---------------------------------
+      // GIF Signature
+
+      out.writeString('GIF87a');
+
+      //---------------------------------
+      // Screen Descriptor
+
+      out.writeShort(_width);
+      out.writeShort(_height);
+
+      out.writeByte(0x80); // 2bit
+      out.writeByte(0);
+      out.writeByte(0);
+
+      //---------------------------------
+      // Global Color Map
+
+      // black
+      out.writeByte(0x00);
+      out.writeByte(0x00);
+      out.writeByte(0x00);
+
+      // white
+      out.writeByte(0xff);
+      out.writeByte(0xff);
+      out.writeByte(0xff);
+
+      //---------------------------------
+      // Image Descriptor
+
+      out.writeString(',');
+      out.writeShort(0);
+      out.writeShort(0);
+      out.writeShort(_width);
+      out.writeShort(_height);
+      out.writeByte(0);
+
+      //---------------------------------
+      // Local Color Map
+
+      //---------------------------------
+      // Raster Data
+
+      var lzwMinCodeSize = 2;
+      var raster = getLZWRaster(lzwMinCodeSize);
+
+      out.writeByte(lzwMinCodeSize);
+
+      var offset = 0;
+
+      while (raster.length - offset > 255) {
+        out.writeByte(255);
+        out.writeBytes(raster, offset, 255);
+        offset += 255;
+      }
+
+      out.writeByte(raster.length - offset);
+      out.writeBytes(raster, offset, raster.length - offset);
+      out.writeByte(0x00);
+
+      //---------------------------------
+      // GIF Terminator
+      out.writeString(';');
+    };
+
+    var bitOutputStream = function(out) {
+
+      var _out = out;
+      var _bitLength = 0;
+      var _bitBuffer = 0;
+
+      var _this = {};
+
+      _this.write = function(data, length) {
+
+        if ( (data >>> length) != 0) {
+          throw new Error('length over');
+        }
+
+        while (_bitLength + length >= 8) {
+          _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) );
+          length -= (8 - _bitLength);
+          data >>>= (8 - _bitLength);
+          _bitBuffer = 0;
+          _bitLength = 0;
+        }
+
+        _bitBuffer = (data << _bitLength) | _bitBuffer;
+        _bitLength = _bitLength + length;
+      };
+
+      _this.flush = function() {
+        if (_bitLength > 0) {
+          _out.writeByte(_bitBuffer);
+        }
+      };
+
+      return _this;
+    };
+
+    var getLZWRaster = function(lzwMinCodeSize) {
+
+      var clearCode = 1 << lzwMinCodeSize;
+      var endCode = (1 << lzwMinCodeSize) + 1;
+      var bitLength = lzwMinCodeSize + 1;
+
+      // Setup LZWTable
+      var table = lzwTable();
+
+      for (var i = 0; i < clearCode; i += 1) {
+        table.add(String.fromCharCode(i) );
+      }
+      table.add(String.fromCharCode(clearCode) );
+      table.add(String.fromCharCode(endCode) );
+
+      var byteOut = byteArrayOutputStream();
+      var bitOut = bitOutputStream(byteOut);
+
+      // clear code
+      bitOut.write(clearCode, bitLength);
+
+      var dataIndex = 0;
+
+      var s = String.fromCharCode(_data[dataIndex]);
+      dataIndex += 1;
+
+      while (dataIndex < _data.length) {
+
+        var c = String.fromCharCode(_data[dataIndex]);
+        dataIndex += 1;
+
+        if (table.contains(s + c) ) {
+
+          s = s + c;
+
+        } else {
+
+          bitOut.write(table.indexOf(s), bitLength);
+
+          if (table.size() < 0xfff) {
+
+            if (table.size() == (1 << bitLength) ) {
+              bitLength += 1;
+            }
+
+            table.add(s + c);
+          }
+
+          s = c;
+        }
+      }
+
+      bitOut.write(table.indexOf(s), bitLength);
+
+      // end code
+      bitOut.write(endCode, bitLength);
+
+      bitOut.flush();
+
+      return byteOut.toByteArray();
+    };
+
+    var lzwTable = function() {
+
+      var _map = {};
+      var _size = 0;
+
+      var _this = {};
+
+      _this.add = function(key) {
+        if (_this.contains(key) ) {
+          throw new Error('dup key:' + key);
+        }
+        _map[key] = _size;
+        _size += 1;
+      };
+
+      _this.size = function() {
+        return _size;
+      };
+
+      _this.indexOf = function(key) {
+        return _map[key];
+      };
+
+      _this.contains = function(key) {
+        return typeof _map[key] != 'undefined';
+      };
+
+      return _this;
+    };
+
+    return _this;
+  };
+
+  var createImgTag = function(width, height, getPixel, alt) {
+
+    var gif = gifImage(width, height);
+    for (var y = 0; y < height; y += 1) {
+      for (var x = 0; x < width; x += 1) {
+        gif.setPixel(x, y, getPixel(x, y) );
+      }
+    }
+
+    var b = byteArrayOutputStream();
+    gif.write(b);
+
+    var base64 = base64EncodeOutputStream();
+    var bytes = b.toByteArray();
+    for (var i = 0; i < bytes.length; i += 1) {
+      base64.writeByte(bytes[i]);
+    }
+    base64.flush();
+
+    var img = '';
+    img += '<img';
+    img += '\u0020src="';
+    img += 'data:image/gif;base64,';
+    img += base64;
+    img += '"';
+    img += '\u0020width="';
+    img += width;
+    img += '"';
+    img += '\u0020height="';
+    img += height;
+    img += '"';
+    if (alt) {
+      img += '\u0020alt="';
+      img += alt;
+      img += '"';
+    }
+    img += '/>';
+
+    return img;
+  };
+
+  //---------------------------------------------------------------------
+  // returns qrcode function.
+
+  return qrcode;
+}();
+
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+      define([], factory);
+  } else if (typeof exports === 'object') {
+      module.exports = factory();
+  }
+}(function () {
+    return qrcode;
+}));
+</script>
+<script>
+// http://www.movable-type.co.uk/scripts/sha256.html
+// local modifications: removed msg = msg.utf8Encode(); from start of Sha256.hash
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+/*  SHA-256 implementation in JavaScript                (c) Chris Veness 2002-2014 / MIT Licence  */
+/*                                                                                                */
+/*  - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html                              */
+/*        http://csrc.nist.gov/groups/ST/toolkit/examples.html                                    */
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+
+/* jshint node:true *//* global define, escape, unescape */
+'use strict';
+
+
+/**
+ * SHA-256 hash function reference implementation.
+ *
+ * @namespace
+ */
+var Sha256 = {};
+
+
+/**
+ * Generates SHA-256 hash of string.
+ *
+ * @param   {string} msg - String to be hashed
+ * @returns {string} Hash of msg as hex character string
+ */
+Sha256.hash = function(msg) {
+    // convert string to UTF-8, as SHA only deals with byte-streams
+    //msg = msg.utf8Encode();
+
+    // constants [§4.2.2]
+    var K = [
+        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+        0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+        0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+        0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+        0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+        0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+        0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ];
+    // initial hash value [§5.3.1]
+    var H = [
+        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ];
+
+    // PREPROCESSING
+
+    msg += String.fromCharCode(0x80);  // add trailing '1' bit (+ 0's padding) to string [§5.1.1]
+
+    // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
+    var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length
+    var N = Math.ceil(l/16);  // number of 16-integer-blocks required to hold 'l' ints
+    var M = new Array(N);
+
+    for (var i=0; i<N; i++) {
+        M[i] = new Array(16);
+        for (var j=0; j<16; j++) {  // encode 4 chars per integer, big-endian encoding
+            M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |
+                      (msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));
+        } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0
+    }
+    // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]
+    // note: most significant word would be (len-1)*8 >>> 32, but since JS converts
+    // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
+    M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]);
+    M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
+
+
+    // HASH COMPUTATION [§6.1.2]
+
+    var W = new Array(64); var a, b, c, d, e, f, g, h;
+    for (var i=0; i<N; i++) {
+
+        // 1 - prepare message schedule 'W'
+        for (var t=0;  t<16; t++) W[t] = M[i][t];
+        for (var t=16; t<64; t++) W[t] = (Sha256.σ1(W[t-2]) + W[t-7] + Sha256.σ0(W[t-15]) + W[t-16]) & 0xffffffff;
+
+        // 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value
+        a = H[0]; b = H[1]; c = H[2]; d = H[3]; e = H[4]; f = H[5]; g = H[6]; h = H[7];
+
+        // 3 - main loop (note 'addition modulo 2^32')
+        for (var t=0; t<64; t++) {
+            var T1 = h + Sha256.Σ1(e) + Sha256.Ch(e, f, g) + K[t] + W[t];
+            var T2 =     Sha256.Σ0(a) + Sha256.Maj(a, b, c);
+            h = g;
+            g = f;
+            f = e;
+            e = (d + T1) & 0xffffffff;
+            d = c;
+            c = b;
+            b = a;
+            a = (T1 + T2) & 0xffffffff;
+        }
+         // 4 - compute the new intermediate hash value (note 'addition modulo 2^32')
+        H[0] = (H[0]+a) & 0xffffffff;
+        H[1] = (H[1]+b) & 0xffffffff;
+        H[2] = (H[2]+c) & 0xffffffff;
+        H[3] = (H[3]+d) & 0xffffffff;
+        H[4] = (H[4]+e) & 0xffffffff;
+        H[5] = (H[5]+f) & 0xffffffff;
+        H[6] = (H[6]+g) & 0xffffffff;
+        H[7] = (H[7]+h) & 0xffffffff;
+    }
+
+    return Sha256.toHexStr(H[0]) + Sha256.toHexStr(H[1]) + Sha256.toHexStr(H[2]) + Sha256.toHexStr(H[3]) +
+           Sha256.toHexStr(H[4]) + Sha256.toHexStr(H[5]) + Sha256.toHexStr(H[6]) + Sha256.toHexStr(H[7]);
+};
+
+
+/**
+ * Rotates right (circular right shift) value x by n positions [§3.2.4].
+ * @private
+ */
+Sha256.ROTR = function(n, x) {
+    return (x >>> n) | (x << (32-n));
+};
+
+/**
+ * Logical functions [§4.1.2].
+ * @private
+ */
+Sha256.Σ0  = function(x) { return Sha256.ROTR(2,  x) ^ Sha256.ROTR(13, x) ^ Sha256.ROTR(22, x); };
+Sha256.Σ1  = function(x) { return Sha256.ROTR(6,  x) ^ Sha256.ROTR(11, x) ^ Sha256.ROTR(25, x); };
+Sha256.σ0  = function(x) { return Sha256.ROTR(7,  x) ^ Sha256.ROTR(18, x) ^ (x>>>3);  };
+Sha256.σ1  = function(x) { return Sha256.ROTR(17, x) ^ Sha256.ROTR(19, x) ^ (x>>>10); };
+Sha256.Ch  = function(x, y, z) { return (x & y) ^ (~x & z); };
+Sha256.Maj = function(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); };
+
+
+/**
+ * Hexadecimal representation of a number.
+ * @private
+ */
+Sha256.toHexStr = function(n) {
+    // note can't use toString(16) as it is implementation-dependant,
+    // and in IE returns signed numbers when used on full words
+    var s="", v;
+    for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); }
+    return s;
+};
+
+
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+
+
+/** Extend String object with method to encode multi-byte string to utf8
+ *  - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */
+if (typeof String.prototype.utf8Encode == 'undefined') {
+    String.prototype.utf8Encode = function() {
+        return unescape( encodeURIComponent( this ) );
+    };
+}
+
+/** Extend String object with method to decode utf8 string to multi-byte */
+if (typeof String.prototype.utf8Decode == 'undefined') {
+    String.prototype.utf8Decode = function() {
+        try {
+            return decodeURIComponent( escape( this ) );
+        } catch (e) {
+            return this; // invalid UTF-8? return as-is
+        }
+    };
+}
+
+
+/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
+if (typeof module != 'undefined' && module.exports) module.exports = Sha256; // CommonJs export
+if (typeof define == 'function' && define.amd) define([], function() { return Sha256; }); // AMD
+</script>
+<style>
+
+    #typein {
+        position: absolute;
+        font-family: monospace;
+        font-size: 10mm;
+        transform-origin: 0 0;
+        white-space: pre;
+    }
+
+    .columns2 {
+        -moz-column-count: 2;
+        -webkit-column-count: 2;
+        column-count: 2;
+        -webkit-column-rule: 4mm outset #ddd;
+        -moz-column-rule: 4mm outset #ddd;
+        column-rule: 4mm outset #ddd;
+    }
+
+    #title {
+        font-size: 6mm;
+    }
+
+
+    .placeholder {
+        color: #ddd;
+    }
+
+    @media print {
+        @page {
+            margin: 0mm; /* disable url etc headers and footers (e.g. chrome, newer firefox) */
+        }
+
+        html {
+            margin: 15mm 20mm;
+            padding: 0px;
+        }
+        * {
+            -webkit-print-color-adjust: exact;
+        }
+
+        #settings {
+            display: none;
+        }
+
+        #printout, body {
+            margin: 0px;
+            padding: 0px;
+        }
+
+        #lowermargin {
+            display: none;
+        }
+
+    }
+
+    @media screen, projection {
+        *[contenteditable] {
+            cursor: pointer;
+            border: 1px dotted #A4DDED;
+        }
+        *[contenteditable]:hover,
+        *[contenteditable]:focus {
+            background: #DEF;
+            box-shadow: 0 0 1em 0.5em #DEF;
+        }
+
+        html {
+            background: #999;
+            padding: 0.5in;
+        }
+
+        #settings {
+            box-sizing: border-box;
+            margin: 0 auto;
+            background: #FFF;
+            border-radius: 1px;
+            box-shadow: 0 0 1in -0.25in rgba(0, 0, 0, 0.5);
+
+            margin-bottom: 2cm;
+            padding: 10px;
+        }
+
+        .block {
+            display: inline-block;
+            vertical-align: top;
+        }
+
+        .label {
+            display: inline-block;
+            min-width: 50mm;
+        }
+
+        #keyfile, #keyfile_expander {
+            min-width: 140mm;
+        }
+
+        .instructions {
+            max-width: 140mm;
+            padding-bottom: 1rem;
+        }
+
+        #fileinput {
+            display: none;
+        }
+
+        #printout {
+            box-sizing: border-box;
+            height: 279mm; /* smallest of US Letter */
+            width: 210mm; /* and DIN/ISO A4 */
+            /*height: 297mm; use this for real A4 */
+            margin: 0 auto;
+            overflow: hidden;
+            padding: 15mm 20mm 0mm 20mm;
+            background: #FFF;
+            border-radius: 1px;
+            box-shadow: 0 0 1in -0.25in rgba(0, 0, 0, 0.5);
+        }
+
+        #lowermargin {
+            height: 15mm;
+        }
+    }
+
+    /* center the QR code on the page */
+    #qr {
+      width: 100%;
+      text-align: center;
+    }
+</style>
+</head>
+<body>
+    <div id="settings">
+        <div class="instructions">
+           <p>To create a printable key, either paste the contents of your keyfile or a key export in the text field
+           below, or select a key export file.</p>
+           <p>To create a key export use <pre>borg key export /path/to/repository exportfile.txt</pre></p>
+           <p>If you are using keyfile mode, keyfiles are usually stored in $HOME/.config/borg/keys/</p>
+           <p>You can edit the parts with light blue border in the print preview below by click into them.</p>
+           <p>Key security: This print template will never send anything to remote servers. But keep in mind, that printing
+              might involve computers that can store the printed image, for example with cloud printing services, or
+              networked printers.<p>
+        </div>
+        <div class="block">
+            <div id="keyfile_expander" style="display:none;"><a href="#" onclick="document.getElementById('keyfile').style.display='inline';document.getElementById('keyfile_expander').style.display='none';">show keyfile</a></div>
+            <textarea id="keyfile" rows="10" cols="73"></textarea>
+        </div>
+        <div class="block">
+            <span class="label">QR error correction:</span>
+            <select id="errorCorrectionLevel" name="e">
+                <option value="L">7% (L)</option>
+                <option value="M">15% (M)</option>
+                <option value="Q">25% (Q)</option>
+                <option value="H" selected="selected">30% (H)</option>
+            </select><br>
+            <span class="label">QR code size</span><input type="range" id="qrsize" min="45" max="157" value="100"><br>
+            <span class="label">Text size</span><input type="range" id="textsize" min="1" max="60" value="27"><br>
+            <span class="label">Text columns</span>
+            <select id="cols">
+                <option value="1">1</option>
+                <option value="2" selected="selected">2</option>
+            </select><br>
+
+        </div>
+        <div>
+            <input type="file" id="fileinput"/>
+            <input type="button" value="update" onclick="update()"/>
+        </div>
+    </div>
+
+    <div id="printout">
+        <div id="title" contenteditable>BorgBackup Printable Key Backup</div>
+        <div contenteditable>To restore either scan the QR code below, decode it and import it using
+<pre>borg key import /path/to/repo scannedfile</pre>
+
+Or run
+<pre>borg key import --paper /path/to/repo</pre> and type in the text below.<br><br></div>
+        <div id="typein"></div>
+        <div id="spacer"></div>
+        <div id="qr"></div>
+        <div contenteditable>Notes:</div>
+        <div id="lowermargin"></div>
+    </div>
+
+<script>
+"use strict";
+
+function placeholder_qrcode() {
+    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194 194" width="170mm" height="170mm">' +
+                '<path d="m 186,8 0,14 -14,0 0,-14 m 12,12 0,-10 -10,0 0,10 m -162,-8 0,6 6,0 0,-6 m 164,6 -6,0 0,-6 6,0 z M 10,10 20,10 20,20 10,20 M 8,8 8,22 22,22 22,8 m 150,166 0,-2 2,0 0,2 z m 6,-6 -10,0 0,10 10,0 m -8,-8 6,0 0,6 -6,0 z m -158,12 0,-6 6,0 0,6 m 2,-8 0,10 -10,0 0,-10 m 0,0 10,0 m 2,-2 -14,0 0,14 14,0" style="fill: #ddd;"/>' +
+                '<text style="font-size:10px;fill:#ddd;stroke:none;stroke-width:1px;" x="47.321415" y="99.851288">QR Code goes here</text>' +
+            '</svg>';
+}
+
+function create_qrcode(text, errorCorrectionLevel) {
+    for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
+        try {
+            var qr = qrcode(typeNumber, errorCorrectionLevel);
+            qr.addData(text);
+            qr.make();
+            return qr.createSvgTag();
+        } catch (e) {
+            if (!e.message || !e.message.startsWith("code length overflow")) {
+                throw e;
+            }
+        }
+    }
+
+    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194 194" width="194px" height="194px">' +
+                '<path d="m 186,8 0,14 -14,0 0,-14 m 12,12 0,-10 -10,0 0,10 m -162,-8 0,6 6,0 0,-6 m 164,6 -6,0 0,-6 6,0 z M 10,10 20,10 20,20 10,20 M 8,8 8,22 22,22 22,8 m 150,166 0,-2 2,0 0,2 z m 6,-6 -10,0 0,10 10,0 m -8,-8 6,0 0,6 -6,0 z m -158,12 0,-6 6,0 0,6 m 2,-8 0,10 -10,0 0,-10 m 0,0 10,0 m 2,-2 -14,0 0,14 14,0" style="fill: #ddd;"/>' +
+                '<text style="font-size:10px;fill:#ddd;stroke:none;stroke-width:1px;" x="30" y="90">Key too long for this</text>' +
+                '<text style="font-size:10px;fill:#ddd;stroke:none;stroke-width:1px;" x="30" y="110">error correction level</text>' +
+            '</svg>'
+}
+
+function placeholder_paperkey() {
+
+    var res = "BORG PAPER KEY v1\n"
+    res += "id: ?? / ?????? ?????? ?????? / ?????? ?????? - ??\n"
+    for (var i = 1; i <= 19; i++) {
+        res += (i < 10 ? ' ' : '') + i.toString();
+        res += ": ?????? ?????? ?????? ?????? ?????? ?????? - ??\n";
+    }
+
+    return res;
+}
+
+function create_paperkey(text) {
+    var grouped = function(s) {
+        var ret = '';
+        var i = 0
+        for (var ch of s) {
+            if (i && i % 6 == 0) {
+                ret += ' ';
+            }
+            ret += ch;
+            i += 1;
+        }
+        return ret;
+    };
+    var toHex = function(b) {
+        var ret = '';
+        for (var ch of b) {
+            var tmp = ch.charCodeAt(0).toString(16);
+            if (tmp.length == 0) {
+                ret += '00';
+            } else if (tmp.length == 1) {
+                ret += '0';
+            }
+            ret += tmp;
+        }
+        return ret;
+    }
+    var sha256_truncated = function(b, digits) {
+        return Sha256.hash(b).substr(0, digits);
+    };
+
+    var export_ = '';
+
+    var lines = text.split('\n');
+    var first_line = lines.shift();
+
+    var binary = atob(lines.join('\n'));
+    export_ += 'BORG PAPER KEY v1\n';
+    lines = Math.ceil(binary.length / 18);
+    var repoid = first_line.substr(9, 18)
+    var complete_checksum = sha256_truncated(binary, 12)
+    export_ += 'id: ';
+    export_ += lines.toString();
+    var checksum = sha256_truncated(lines.toString() + '/' + repoid + '/' + complete_checksum, 2);
+    export_ += ' / ' + grouped(repoid) + ' / ' + grouped(complete_checksum) + ' - ' + checksum +  '\n';
+
+    var idx = 0
+    while (binary.length) {
+        idx += 1
+        var binline = binary.substr(0, 18);
+        checksum = sha256_truncated(String.fromCharCode(idx >> 8) + String.fromCharCode(idx & 0xff) + binline, 2);
+        export_ += (idx < 10 ? ' ' : '') + idx.toString();
+        export_ += ': ' + grouped(toHex(binline)) + ' - ' + checksum + '\n';
+        binary = binary.substr(18);
+    }
+
+    return export_;
+}
+
+function update() {
+    var text = document.getElementById('keyfile').value;
+    var target = document.getElementById('qr');
+    var typein = document.getElementById('typein');
+
+    if (!text) {
+        typein.innerText = placeholder_paperkey()
+        target.innerHTML = placeholder_qrcode();
+        var svg = target.children[0];
+
+        typein.classList.add("placeholder");
+        svg.classList.add("placeholder");
+    } else {
+        var e = document.getElementById('errorCorrectionLevel').value;
+        target.innerHTML = create_qrcode(text, e);
+        var svg = target.children[0];
+        svg.setAttribute("viewBox", "0 0 " + svg.getAttribute("width").replace("px", "")
+                        + " " + svg.getAttribute("height").replace("px", ""));
+
+        typein.innerText = create_paperkey(text);
+
+        typein.classList.remove("placeholder");
+        svg.classList.remove("placeholder");
+    }
+
+    var printout = document.getElementById('printout');
+    var size = document.getElementById('qrsize').value;
+
+    typein.style.transform = "scale(" + document.getElementById('textsize').value / 100 + ")";
+    if (document.getElementById('cols').value == "2") {
+        typein.classList.add("columns2");
+    } else {
+        typein.classList.remove("columns2");
+    }
+    document.getElementById('spacer').style.height = typein.getBoundingClientRect().height + "px";
+
+    while (true) {
+        svg.setAttribute("width", size + "mm");
+        svg.setAttribute("height", size + "mm");
+
+        if (printout.scrollHeight <= printout.clientHeight || size <= 50) {
+            break;
+        }
+        size -= 1;
+    }
+
+};
+
+function call_soon(func, wait) {
+    var timeout;
+    return function() {
+        clearTimeout(timeout);
+        timeout = setTimeout(function() {
+            timeout = null;
+            func();
+        }, wait);
+    };
+};
+
+function updateFromFile() {
+    var f = document.getElementById('fileinput').files[0];
+
+    if (f) {
+        var r = new FileReader();
+        r.onload = function(ev) {
+            var contents = ev.target.result;
+            document.getElementById('keyfile').value = contents;
+            document.getElementById('keyfile').style.display='none';
+            document.getElementById('keyfile_expander').style.display='block';
+            update();
+        };
+        r.readAsText(f);
+    }
+}
+
+if (window.File && window.FileReader && window.FileList && window.Blob) {
+    document.getElementById('fileinput').style.display='inline';
+    document.getElementById('fileinput').addEventListener('change', updateFromFile, false);
+
+    if (document.getElementById('fileinput').files.length) {
+        updateFromFile();
+    }
+}
+
+function loaded() {
+    document.getElementById('qrsize').addEventListener('change', update, false);
+    document.getElementById('qrsize').addEventListener('input', call_soon(update), false);
+    document.getElementById('textsize').addEventListener('change', update, false);
+    document.getElementById('textsize').addEventListener('input', call_soon(update), false);
+
+    document.getElementById('cols').addEventListener('change', update, false);
+    document.getElementById('errorCorrectionLevel').addEventListener('change', update, false);
+
+    var editables = document.querySelectorAll("*[contenteditable]");
+    for (var i = 0; i < editables.length; ++i) {
+        editables[i].addEventListener('input', call_soon(update), false);
+    }
+
+    update();
+}
+
+window.addEventListener("load", loaded, false);
+
+</script>
+
+</body>
+</html>

+ 60 - 0
src/borg/testsuite/archiver.py

@@ -877,6 +877,53 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         os.mkdir('input/cache3')
         os.mkdir('input/cache3')
         os.link('input/cache1/%s' % CACHE_TAG_NAME, 'input/cache3/%s' % CACHE_TAG_NAME)
         os.link('input/cache1/%s' % CACHE_TAG_NAME, 'input/cache3/%s' % CACHE_TAG_NAME)
 
 
+    def test_create_without_root(self):
+        """test create without a root"""
+        self.cmd('init', self.repository_location)
+        args = ['create', self.repository_location + '::test']
+        if self.FORK_DEFAULT:
+            self.cmd(*args, exit_code=2)
+        else:
+            self.assert_raises(SystemExit, lambda: self.cmd(*args))
+
+    def test_create_pattern_root(self):
+        """test create with only a root pattern"""
+        self.cmd('init', self.repository_location)
+        self.create_regular_file('file1', size=1024 * 80)
+        self.create_regular_file('file2', size=1024 * 80)
+        output = self.cmd('create', '-v', '--list', '--pattern=R input', self.repository_location + '::test')
+        self.assert_in("A input/file1", output)
+        self.assert_in("A input/file2", output)
+
+    def test_create_pattern(self):
+        """test file patterns during create"""
+        self.cmd('init', self.repository_location)
+        self.create_regular_file('file1', size=1024 * 80)
+        self.create_regular_file('file2', size=1024 * 80)
+        self.create_regular_file('file_important', size=1024 * 80)
+        output = self.cmd('create', '-v', '--list',
+                          '--pattern=+input/file_important', '--pattern=-input/file*',
+                          self.repository_location + '::test', 'input')
+        self.assert_in("A input/file_important", output)
+        self.assert_in("A input/file_important", output)
+        self.assert_in('x input/file1', output)
+        self.assert_in('x input/file2', output)
+
+    def test_extract_pattern_opt(self):
+        self.cmd('init', self.repository_location)
+        self.create_regular_file('file1', size=1024 * 80)
+        self.create_regular_file('file2', size=1024 * 80)
+        self.create_regular_file('file_important', size=1024 * 80)
+        self.cmd('create', self.repository_location + '::test', 'input')
+        with changedir('output'):
+            self.cmd('extract',
+                     '--pattern=+input/file_important', '--pattern=-input/file*',
+                     self.repository_location + '::test')
+        self.assert_equal(sorted(os.listdir('output/input')), ['file_important'])
+
+    def test_exclude_caches(self):
+        self.cmd('init', self.repository_location)
+
     def _assert_test_caches(self):
     def _assert_test_caches(self):
         with changedir('output'):
         with changedir('output'):
             self.cmd('extract', self.repository_location + '::test')
             self.cmd('extract', self.repository_location + '::test')
@@ -1973,6 +2020,19 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
 
         assert repo_key2.enc_key == repo_key2.enc_key
         assert repo_key2.enc_key == repo_key2.enc_key
 
 
+    def test_key_export_qr(self):
+        export_file = self.output_path + '/exported.html'
+        self.cmd('init', self.repository_location, '--encryption', 'repokey')
+        repo_id = self._extract_repository_id(self.repository_path)
+        self.cmd('key', 'export', '--qr-html', self.repository_location, export_file)
+
+        with open(export_file, 'r', encoding='utf-8') as fd:
+            export_contents = fd.read()
+
+        assert bin_to_hex(repo_id) in export_contents
+        assert export_contents.startswith('<!doctype html>')
+        assert export_contents.endswith('</html>')
+
     def test_key_import_errors(self):
     def test_key_import_errors(self):
         export_file = self.output_path + '/exported'
         export_file = self.output_path + '/exported'
         self.cmd('init', self.repository_location, '--encryption', 'keyfile')
         self.cmd('init', self.repository_location, '--encryption', 'keyfile')

+ 110 - 4
src/borg/testsuite/helpers.py

@@ -1,11 +1,12 @@
+import argparse
 import hashlib
 import hashlib
-import logging
 import os
 import os
 import sys
 import sys
 from datetime import datetime, timezone, timedelta
 from datetime import datetime, timezone, timedelta
 from time import mktime, strptime, sleep
 from time import mktime, strptime, sleep
 
 
 import pytest
 import pytest
+
 import msgpack
 import msgpack
 import msgpack.fallback
 import msgpack.fallback
 
 
@@ -21,7 +22,7 @@ from ..helpers import yes, TRUISH, FALSISH, DEFAULTISH
 from ..helpers import StableDict, bin_to_hex
 from ..helpers import StableDict, bin_to_hex
 from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk
 from ..helpers import parse_timestamp, ChunkIteratorFileWrapper, ChunkerParams, Chunk
 from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless
 from ..helpers import ProgressIndicatorPercent, ProgressIndicatorEndless
-from ..helpers import load_excludes
+from ..helpers import load_exclude_file, load_pattern_file
 from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2
 from ..helpers import CompressionSpec, CompressionDecider1, CompressionDecider2
 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern
 from ..helpers import parse_pattern, PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern
 from ..helpers import swidth_slice
 from ..helpers import swidth_slice
@@ -431,8 +432,13 @@ def test_invalid_unicode_pattern(pattern):
     (["pp:/"], [" #/wsfoobar", "\tstart/whitespace"]),
     (["pp:/"], [" #/wsfoobar", "\tstart/whitespace"]),
     (["pp:aaabbb"], None),
     (["pp:aaabbb"], None),
     (["pp:/data", "pp: #/", "pp:\tstart", "pp:/whitespace"], ["/more/data", "/home"]),
     (["pp:/data", "pp: #/", "pp:\tstart", "pp:/whitespace"], ["/more/data", "/home"]),
+    (["/nomatch", "/more/*"],
+     ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']),
+    # the order of exclude patterns shouldn't matter
+    (["/more/*", "/nomatch"],
+     ['/data/something00.txt', '/home', ' #/wsfoobar', '\tstart/whitespace', '/whitespace/end\t']),
     ])
     ])
-def test_patterns_from_file(tmpdir, lines, expected):
+def test_exclude_patterns_from_file(tmpdir, lines, expected):
     files = [
     files = [
         '/data/something00.txt', '/more/data', '/home',
         '/data/something00.txt', '/more/data', '/home',
         ' #/wsfoobar',
         ' #/wsfoobar',
@@ -441,8 +447,10 @@ def test_patterns_from_file(tmpdir, lines, expected):
     ]
     ]
 
 
     def evaluate(filename):
     def evaluate(filename):
+        patterns = []
+        load_exclude_file(open(filename, "rt"), patterns)
         matcher = PatternMatcher(fallback=True)
         matcher = PatternMatcher(fallback=True)
-        matcher.add(load_excludes(open(filename, "rt")), False)
+        matcher.add_inclexcl(patterns)
         return [path for path in files if matcher.match(path)]
         return [path for path in files if matcher.match(path)]
 
 
     exclfile = tmpdir.join("exclude.txt")
     exclfile = tmpdir.join("exclude.txt")
@@ -453,6 +461,104 @@ def test_patterns_from_file(tmpdir, lines, expected):
     assert evaluate(str(exclfile)) == (files if expected is None else expected)
     assert evaluate(str(exclfile)) == (files if expected is None else expected)
 
 
 
 
+@pytest.mark.parametrize("lines, expected_roots, expected_numpatterns", [
+    # "None" means all files, i.e. none excluded
+    ([], [], 0),
+    (["# Comment only"], [], 0),
+    (["- *"], [], 1),
+    (["+fm:*/something00.txt",
+      "-/data"], [], 2),
+    (["R /"], ["/"], 0),
+    (["R /",
+      "# comment"], ["/"], 0),
+    (["# comment",
+      "- /data",
+      "R /home"], ["/home"], 1),
+])
+def test_load_patterns_from_file(tmpdir, lines, expected_roots, expected_numpatterns):
+    def evaluate(filename):
+        roots = []
+        inclexclpatterns = []
+        load_pattern_file(open(filename, "rt"), roots, inclexclpatterns)
+        return roots, len(inclexclpatterns)
+    patternfile = tmpdir.join("patterns.txt")
+
+    with patternfile.open("wt") as fh:
+        fh.write("\n".join(lines))
+
+    roots, numpatterns = evaluate(str(patternfile))
+    assert roots == expected_roots
+    assert numpatterns == expected_numpatterns
+
+
+@pytest.mark.parametrize("lines", [
+    (["X /data"]),  # illegal pattern type prefix
+    (["/data"]),    # need a pattern type prefix
+])
+def test_load_invalid_patterns_from_file(tmpdir, lines):
+    patternfile = tmpdir.join("patterns.txt")
+    with patternfile.open("wt") as fh:
+        fh.write("\n".join(lines))
+    filename = str(patternfile)
+    with pytest.raises(argparse.ArgumentTypeError):
+        roots = []
+        inclexclpatterns = []
+        load_pattern_file(open(filename, "rt"), roots, inclexclpatterns)
+
+
+@pytest.mark.parametrize("lines, expected", [
+    # "None" means all files, i.e. none excluded
+    ([], None),
+    (["# Comment only"], None),
+    (["- *"], []),
+    # default match type is sh: for patterns -> * doesn't match a /
+    (["-*/something0?.txt"],
+     ['/data', '/data/something00.txt', '/data/subdir/something01.txt',
+      '/home', '/home/leo', '/home/leo/t', '/home/other']),
+    (["-fm:*/something00.txt"],
+     ['/data', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t', '/home/other']),
+    (["-fm:*/something0?.txt"],
+     ["/data", '/home', '/home/leo', '/home/leo/t', '/home/other']),
+    (["+/*/something0?.txt",
+      "-/data"],
+     ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']),
+    (["+fm:*/something00.txt",
+      "-/data"],
+     ["/data/something00.txt", '/home', '/home/leo', '/home/leo/t', '/home/other']),
+    # include /home/leo and exclude the rest of /home:
+    (["+/home/leo",
+      "-/home/*"],
+     ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']),
+    # wrong order, /home/leo is already excluded by -/home/*:
+    (["-/home/*",
+      "+/home/leo"],
+     ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home']),
+    (["+fm:/home/leo",
+      "-/home/"],
+     ['/data', '/data/something00.txt', '/data/subdir/something01.txt', '/home', '/home/leo', '/home/leo/t']),
+])
+def test_inclexcl_patterns_from_file(tmpdir, lines, expected):
+    files = [
+        '/data', '/data/something00.txt', '/data/subdir/something01.txt',
+        '/home', '/home/leo', '/home/leo/t', '/home/other'
+    ]
+
+    def evaluate(filename):
+        matcher = PatternMatcher(fallback=True)
+        roots = []
+        inclexclpatterns = []
+        load_pattern_file(open(filename, "rt"), roots, inclexclpatterns)
+        matcher.add_inclexcl(inclexclpatterns)
+        return [path for path in files if matcher.match(path)]
+
+    patternfile = tmpdir.join("patterns.txt")
+
+    with patternfile.open("wt") as fh:
+        fh.write("\n".join(lines))
+
+    assert evaluate(str(patternfile)) == (files if expected is None else expected)
+
+
 @pytest.mark.parametrize("pattern, cls", [
 @pytest.mark.parametrize("pattern, cls", [
     ("", FnmatchPattern),
     ("", FnmatchPattern),