Merge lp:~mterry/duplicity/gpg-encode into lp:duplicity/0.6-series

Proposed by Michael Terry on 2014-02-04
Status: Merged
Merged at revision: 961
Proposed branch: lp:~mterry/duplicity/gpg-encode
Merge into: lp:duplicity/0.6-series
Diff against target: 215 lines (+66/-22)
5 files modified
bin/duplicity (+12/-8)
bin/rdiffdir (+0/-3)
testing/overrides/gettext.py (+34/-0)
testing/run-tests (+2/-2)
testing/tests/finaltest.py (+18/-9)
To merge this branch: bzr merge lp:~mterry/duplicity/gpg-encode
Reviewer Review Type Date Requested Status
duplicity-team 2014-02-04 Pending
Review via email: mp+204808@code.launchpad.net

Description of the change

getpass.getpass(prompt) eventually calls str(prompt). Which is a no go, if the prompt contains unicode. Here's a patch to always pass getpass() a byte string.

Our tests didn't catch this because they always set PASSPHRASE. I've added a test that passes the passphrase via stdin.

In order to actually test the bug condition (_() returning unicode strings that can't convert to ascii), I added a gettext.py override that always adds an invisible unicode character to returned strings when testing. This way, we should be able to catch future ascii translation problems in any of the code under test.

Oh, and while I was at it, I removed the useless gettext.install() lines in bin/duplicity and bin/rdiffdir (the gettext.install() call in duplicity/__init__.py is what matters).

To post a comment you must log in.
Michael Terry (mterry) wrote :

Er, I'll just add one with this branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/duplicity'
2--- bin/duplicity 2014-01-21 21:04:27 +0000
3+++ bin/duplicity 2014-02-05 02:57:13 +0000
4@@ -27,7 +27,7 @@
5 # Please send mail to me or the mailing list if you find bugs or have
6 # any suggestions.
7
8-import getpass, gzip, os, sys, time, types
9+import gzip, os, sys, time, types
10 import traceback, platform, statvfs, resource, re
11 import threading
12 from datetime import datetime
13@@ -37,9 +37,6 @@
14 if os.path.exists(os.path.join(pwd, "../duplicity")):
15 sys.path.insert(0, os.path.abspath(os.path.join(pwd, "../.")))
16
17-import gettext
18-gettext.install('duplicity', codeset='utf8')
19-
20 from duplicity import log
21 log.setup()
22
23@@ -65,6 +62,13 @@
24 # If exit_val is not None, exit with given value at end.
25 exit_val = None
26
27+def getpass_safe(message):
28+ # getpass() in Python 2.x will call str() on our prompt. So we can't pass
29+ # in non-ascii characters.
30+ import getpass, locale
31+ message = message.encode(locale.getpreferredencoding(), 'replace')
32+ return getpass.getpass(message)
33+
34 def get_passphrase(n, action, for_signing = False):
35 """
36 Check to make sure passphrase is indeed needed, then get
37@@ -160,19 +164,19 @@
38 if use_cache and globals.gpg_profile.signing_passphrase:
39 pass1 = globals.gpg_profile.signing_passphrase
40 else:
41- pass1 = getpass.getpass(_("GnuPG passphrase for signing key:")+" ")
42+ pass1 = getpass_safe(_("GnuPG passphrase for signing key:")+" ")
43 else:
44 if use_cache and globals.gpg_profile.passphrase:
45 pass1 = globals.gpg_profile.passphrase
46 else:
47- pass1 = getpass.getpass(_("GnuPG passphrase:")+" ")
48+ pass1 = getpass_safe(_("GnuPG passphrase:")+" ")
49
50 if n == 1:
51 pass2 = pass1
52 elif for_signing:
53- pass2 = getpass.getpass(_("Retype passphrase for signing key to confirm: "))
54+ pass2 = getpass_safe(_("Retype passphrase for signing key to confirm: "))
55 else:
56- pass2 = getpass.getpass(_("Retype passphrase to confirm: "))
57+ pass2 = getpass_safe(_("Retype passphrase to confirm: "))
58
59 if not pass1 == pass2:
60 print _("First and second passphrases do not match! Please try again.")
61
62=== modified file 'bin/rdiffdir'
63--- bin/rdiffdir 2013-12-27 06:39:00 +0000
64+++ bin/rdiffdir 2014-02-05 02:57:13 +0000
65@@ -27,9 +27,6 @@
66
67 import sys, getopt, gzip, os
68
69-import gettext
70-gettext.install('duplicity', codeset='utf8')
71-
72 from duplicity import diffdir
73 from duplicity import patchdir
74 from duplicity import log
75
76=== added directory 'testing/overrides'
77=== added file 'testing/overrides/gettext.py'
78--- testing/overrides/gettext.py 1970-01-01 00:00:00 +0000
79+++ testing/overrides/gettext.py 2014-02-05 02:57:13 +0000
80@@ -0,0 +1,34 @@
81+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf8 -*-
82+#
83+# Copyright 2014 Michael Terry <mike@mterry.name>
84+#
85+# This file is part of duplicity.
86+#
87+# Duplicity is free software; you can redistribute it and/or modify it
88+# under the terms of the GNU General Public License as published by the
89+# Free Software Foundation; either version 2 of the License, or (at your
90+# option) any later version.
91+#
92+# Duplicity is distributed in the hope that it will be useful, but
93+# WITHOUT ANY WARRANTY; without even the implied warranty of
94+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
95+# General Public License for more details.
96+#
97+# You should have received a copy of the GNU General Public License
98+# along with duplicity; if not, write to the Free Software Foundation,
99+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
100+
101+# This is just a small override to the system gettext.py which allows us to
102+# always return a string with fancy unicode characters, which will notify us
103+# if we ever get a unicode->ascii translation by accident.
104+
105+def translation(*args, **kwargs):
106+ class Translation:
107+ ZWSP = u"‚Äč" # ZERO WIDTH SPACE, basically an invisible space separator
108+ def install(self, **kwargs):
109+ import __builtin__
110+ __builtin__.__dict__['_'] = lambda x: x + self.ZWSP
111+ def ungettext(self, one, more, n):
112+ if n == 1: return one + self.ZWSP
113+ else: return more + self.ZWSP
114+ return Translation()
115
116=== modified file 'testing/run-tests'
117--- testing/run-tests 2011-11-24 01:49:53 +0000
118+++ testing/run-tests 2014-02-05 02:57:13 +0000
119@@ -25,9 +25,9 @@
120
121 THISDIR=$(pwd)
122 export TZ=US/Central
123-export LANG=
124+export LANG=en_US.UTF-8
125 # up for 'duplicity' module and here for 'helper.py'
126-export PYTHONPATH="$(dirname $THISDIR):$THISDIR/helpers"
127+export PYTHONPATH="$THISDIR/overrides:$(dirname $THISDIR):$THISDIR/helpers"
128 export GNUPGHOME="$THISDIR/gnupg"
129 export PATH="$(dirname $THISDIR)/bin:$PATH"
130
131
132=== modified file 'testing/tests/finaltest.py'
133--- testing/tests/finaltest.py 2012-11-24 19:45:09 +0000
134+++ testing/tests/finaltest.py 2014-02-05 02:57:13 +0000
135@@ -20,6 +20,7 @@
136 # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
137
138 import helper
139+import pexpect
140 import sys, os, unittest
141
142 import duplicity.backend
143@@ -50,7 +51,8 @@
144 """
145 Test backup/restore using duplicity binary
146 """
147- def run_duplicity(self, arglist, options = [], current_time = None):
148+ def run_duplicity(self, arglist, options = [], current_time = None,
149+ passphrase_input = None):
150 """Run duplicity binary with given arguments and options"""
151 options.append("--archive-dir testfiles/cache")
152 cmd_list = ["duplicity"]
153@@ -62,22 +64,23 @@
154 cmd_list.extend(arglist)
155 cmdline = " ".join(cmd_list)
156 #print "Running '%s'." % cmdline
157- if not os.environ.has_key('PASSPHRASE'):
158+ if passphrase_input is None and not os.environ.has_key('PASSPHRASE'):
159 os.environ['PASSPHRASE'] = 'foobar'
160- return_val = os.system(cmdline)
161+ (output, return_val) = pexpect.run(cmdline, withexitstatus=True,
162+ events={'passphrase.*:': passphrase_input})
163 if return_val:
164 raise CmdError(return_val)
165
166- def backup(self, type, input_dir, options = [], current_time = None):
167+ def backup(self, type, input_dir, options = [], **kwargs):
168 """Run duplicity backup to default directory"""
169 options = options[:]
170 if type == "full":
171 options.insert(0, 'full')
172 args = [input_dir, "'%s'" % backend_url]
173- self.run_duplicity(args, options, current_time)
174+ self.run_duplicity(args, options, **kwargs)
175
176 def restore(self, file_to_restore = None, time = None, options = [],
177- current_time = None):
178+ **kwargs):
179 options = options[:] # just nip any mutability problems in bud
180 assert not os.system("rm -rf testfiles/restore_out")
181 args = ["'%s'" % backend_url, "testfiles/restore_out"]
182@@ -85,17 +88,17 @@
183 options.extend(['--file-to-restore', file_to_restore])
184 if time:
185 options.extend(['--restore-time', str(time)])
186- self.run_duplicity(args, options, current_time)
187+ self.run_duplicity(args, options, **kwargs)
188
189 def verify(self, dirname, file_to_verify = None, time = None, options = [],
190- current_time = None):
191+ **kwargs):
192 options = ["verify"] + options[:]
193 args = ["'%s'" % backend_url, dirname]
194 if file_to_verify:
195 options.extend(['--file-to-restore', file_to_verify])
196 if time:
197 options.extend(['--restore-time', str(time)])
198- self.run_duplicity(args, options, current_time)
199+ self.run_duplicity(args, options, **kwargs)
200
201 def deltmp(self):
202 """Delete temporary directories"""
203@@ -255,6 +258,12 @@
204 assert chain.start_time == 30000, chain.start_time
205 assert chain.end_time == 40000, chain.end_time
206
207+ def test_piped_password(self):
208+ """Make sure that prompting for a password works"""
209+ self.backup("full", "testfiles/empty_dir",
210+ passphrase_input="foobar\nfoobar\n")
211+ self.restore(passphrase_input="foobar\n")
212+
213 class FinalTest1(FinalTest, unittest.TestCase):
214 def setUp(self):
215 assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")

Subscribers

People subscribed via source and target branches

to all changes: