Repository: dtrx-py/dtrx (language: python)

Issue:
[feature] Add option `--password` to provide a password
When using non-interactive mode, it is currently impossible to provide a password. It would be wonderful to have an option like `--password` to provide a password using the interactive mode.


Discussion / hints:

Fix patch:
diff --git a/dtrx/dtrx.py b/dtrx/dtrx.py
index 15bd0e5..2c549a1 100755
--- a/dtrx/dtrx.py
+++ b/dtrx/dtrx.py
@@ -237,6 +237,7 @@ def __init__(self, filename, encoding):
         self.filename = os.path.realpath(filename)
         self.encoding = encoding
         self.ignore_pw = False
+        self.password = None
         self.file_count = 0
         self.included_archives = []
         self.target = None
@@ -279,10 +280,9 @@ def wait_for_exit(self, pipe):
             try:
                 return pipe.wait(timeout=1)
             except subprocess.TimeoutExpired:
-                logging.debug("timeout hit..")
+                logging.debug("timeout hit...")
                 self.timeout_check(pipe)
-                # Verify that we're not trying to extract password-protected
-                # archives in non-interactive mode
+                # Verify that we're not waiting for a password in non-interactive mode
                 if self.pw_prompted and self.ignore_pw:
                     pipe.kill()
                     # Whatever extractor we're using probably left the
@@ -292,7 +292,7 @@ def wait_for_exit(self, pipe):
                     self.stderr = ""
                     raise ExtractorError(
                         "cannot extract encrypted archive '%s' in non-interactive mode"
-                        % (self.filename)
+                        " without a password" % (self.filename)
                     )
 
     def send_stdout_to_dev_null(self):
@@ -417,8 +417,9 @@ def extract_archive(self):
         self.pipe(self.extract_pipe)
         self.run_pipes()
 
-    def extract(self, ignore_passwd=False):
+    def extract(self, ignore_passwd=False, password=None):
         self.ignore_pw = ignore_passwd
+        self.password = password
         try:
             self.target = tempfile.mkdtemp(prefix=".dtrx-", dir=".")
         except (OSError, IOError) as error:
@@ -484,8 +485,9 @@ def get_filenames(self):
             raise ExtractorError("doesn't look like a compressed file")
         yield self.basename()
 
-    def extract(self, ignore_passwd=False):
+    def extract(self, ignore_passwd=False, password=None):
         self.ignore_pw = ignore_passwd
+        self.password = password
         self.content_type = ONE_ENTRY_KNOWN
         self.content_name = self.basename()
         self.contents = None
@@ -651,9 +653,18 @@ def get_filenames(self):
 
 class ZipExtractor(NoPipeExtractor):
     file_type = "Zip file"
-    extract_command = ["unzip", "-q"]
     list_command = ["zipinfo", "-1"]
 
+    @property
+    def extract_command(self):
+        """
+        Returns the extraction command and adds a password if given.
+        """
+        cmd = ["unzip", "-q"]
+        if self.password:
+            cmd.append("-P %s" % (self.password,))
+        return cmd
+
     def is_fatal_error(self, status):
         return (status or 0) > 1
 
@@ -702,10 +713,19 @@ def get_filenames(self):
 
 class SevenExtractor(NoPipeExtractor):
     file_type = "7z file"
-    extract_command = ["7z", "x"]
     list_command = ["7z", "l"]
     border_re = re.compile("^[- ]+$")
 
+    @property
+    def extract_command(self):
+        """
+        Returns the extraction command and adds a password if given.
+        """
+        cmd = ["7z", "x"]
+        if self.password:
+            cmd.append("-p%s" % (self.password,))
+        return cmd
+
     def get_filenames(self):
         fn_index = None
         for line in NoPipeExtractor.get_filenames(self):
@@ -809,10 +829,19 @@ def basename(self):
 
 class RarExtractor(NoPipeExtractor):
     file_type = "RAR archive"
-    extract_command = ["unrar", "x"]
     list_command = ["unrar", "v"]
     border_re = re.compile("^-+$")
 
+    @property
+    def extract_command(self):
+        """
+        Returns the extraction command and adds a password if given.
+        """
+        cmd = ["unrar", "x"]
+        if self.password:
+            cmd.append("-p%s" % (self.password,))
+        return cmd
+
     def get_filenames(self):
         inside = False
         isfile = True
@@ -843,9 +872,18 @@ def timeout_check(self, pipe):
 
 class UnarchiverExtractor(NoPipeExtractor):
     file_type = "RAR archive"
-    extract_command = ["unar", "-D"]
     list_command = ["lsar"]
 
+    @property
+    def extract_command(self):
+        """
+        Returns the extraction command and adds a password if given.
+        """
+        cmd = ["unar", "-D"]
+        if self.password:
+            cmd.append("-p %s" % (self.password,))
+        return cmd
+
     def get_filenames(self):
         output = NoPipeExtractor.get_filenames(self)
         next(output)
@@ -856,10 +894,19 @@ def get_filenames(self):
 
 class ArjExtractor(NoPipeExtractor):
     file_type = "ARJ archive"
-    extract_command = ["arj", "x", "-y"]
     list_command = ["arj", "v"]
     prefix_re = re.compile(r"^\d+\)\s+")
 
+    @property
+    def extract_command(self):
+        """
+        Returns the extraction command and adds a password if given.
+        """
+        cmd = ["arj", "x", "-y"]
+        if self.password:
+            cmd.append("-g%s" % (self.password,))
+        return cmd
+
     def get_filenames(self):
         for line in NoPipeExtractor.get_filenames(self):
             match = self.prefix_re.match(line)
@@ -1482,7 +1529,7 @@ def reverser(x, y):
     def run(self, filename, extractor):
         self.current_filename = filename
         error = (
-            self.report(extractor.extract, self.options.batch)
+            self.report(extractor.extract, self.options.batch, self.options.password)
             or self.report(self.get_handler, extractor)
             or self.report(self.current_handler.handle)
             or self.report(self.show_extraction, extractor)
@@ -1632,6 +1679,13 @@ def parse_options(self, arguments):
             default=False,
             help="don't ask how to handle special cases",
         )
+        parser.add_option(
+            "-p",
+            "--password",
+            dest="password",
+            default=None,
+            help="provide a password for password-protected archives",
+        )
         parser.add_option(
             "-o",
             "--overwrite",


Test patch:
diff --git a/tests/test-pw.7z b/tests/test-pw.7z
new file mode 100644
index 0000000..3b231bd
Binary files /dev/null and b/tests/test-pw.7z differ
diff --git a/tests/test-pw.arj b/tests/test-pw.arj
new file mode 100644
index 0000000..10c69d7
Binary files /dev/null and b/tests/test-pw.arj differ
diff --git a/tests/test-pw.rar b/tests/test-pw.rar
new file mode 100644
index 0000000..2520dd2
Binary files /dev/null and b/tests/test-pw.rar differ
diff --git a/tests/tests.yml b/tests/tests.yml
index 95df4c7..5c4c7cd 100644
--- a/tests/tests.yml
+++ b/tests/tests.yml
@@ -865,6 +865,50 @@
   grep: "cannot extract encrypted archive"
   cleanup: stty -F /dev/stdout echo
 
+- name: password zip noninteractive with password
+  filenames: test-pw.zip
+  options: "-n -p yolo"
+  baseline: |
+    mkdir test-pw
+    cd test-pw
+    unzip -P yolo -q $1
+  posttest: |
+    exec [ "$(cat test-pw)" = "test-pw" ]
+  cleanup: rm -rf test-pw
+
+- name: password 7z noninteractive with password
+  filenames: test-pw.7z
+  options: "-n -p yolo"
+  baseline: |
+    mkdir test-pw
+    cd test-pw
+    7z x $1 -pyolo -q
+  posttest: |
+    exec [ "$(cat test-pw)" = "test-pw" ]
+  cleanup: rm -rf test-pw
+
+- name: password rar noninteractive with password
+  filenames: test-pw.rar
+  options: "-n -p yolo"
+  baseline: |
+    mkdir test-pw
+    cd test-pw
+    unrar e -pyolo -q $1
+  posttest: |
+    exec [ "$(cat test-pw)" = "test-pw" ]
+  cleanup: rm -rf test-pw
+
+- name: password arj noninteractive with password
+  filenames: test-pw.7z
+  options: "-n -p yolo"
+  baseline: |
+    mkdir test-pw
+    cd test-pw
+    arj x -gyolo $1
+  posttest: |
+    exec [ "$(cat test-pw)" = "test-pw" ]
+  cleanup: rm -rf test-pw
+
 - name: brotli
   filenames: test-text.br
   baseline: |