1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2
3# Copyright 2015-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
4# Copyright 2015-2018 Antoni Boucher (antoyo) <bouanto@zoho.com>
5#
6# This file is part of qutebrowser.
7#
8# qutebrowser is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# qutebrowser is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.
20
21import os
22import dataclasses
23from typing import List
24
25import pytest
26import bs4
27from PyQt5.QtCore import QUrl
28from PyQt5.QtNetwork import QNetworkRequest
29
30from qutebrowser.browser.webkit.network import filescheme
31from qutebrowser.utils import urlutils, utils
32from helpers import testutils
33
34
35@pytest.mark.parametrize('create_file, create_dir, filterfunc, expected', [
36    (True, False, os.path.isfile, True),
37    (True, False, os.path.isdir, False),
38
39    (False, True, os.path.isfile, False),
40    (False, True, os.path.isdir, True),
41
42    (False, False, os.path.isfile, False),
43    (False, False, os.path.isdir, False),
44])
45def test_get_file_list(tmpdir, create_file, create_dir, filterfunc, expected):
46    """Test get_file_list."""
47    path = tmpdir / 'foo'
48    if create_file or create_dir:
49        path.ensure(dir=create_dir)
50
51    all_files = os.listdir(str(tmpdir))
52
53    result = filescheme.get_file_list(str(tmpdir), all_files, filterfunc)
54    item = {'name': 'foo', 'absname': str(path)}
55    assert (item in result) == expected
56
57
58class TestIsRoot:
59
60    @pytest.mark.windows
61    @pytest.mark.parametrize('directory, is_root', [
62        ('C:\\foo\\bar', False),
63        ('C:\\foo\\', False),
64        ('C:\\foo', False),
65        ('C:\\', True)
66    ])
67    def test_windows(self, directory, is_root):
68        assert filescheme.is_root(directory) == is_root
69
70    @pytest.mark.posix
71    @pytest.mark.parametrize('directory, is_root', [
72        ('/foo/bar', False),
73        ('/foo/', False),
74        ('/foo', False),
75        ('/', True)
76    ])
77    def test_posix(self, directory, is_root):
78        assert filescheme.is_root(directory) == is_root
79
80
81class TestParentDir:
82
83    @pytest.mark.windows
84    @pytest.mark.parametrize('directory, parent', [
85        ('C:\\foo\\bar', 'C:\\foo'),
86        ('C:\\foo', 'C:\\'),
87        ('C:\\foo\\', 'C:\\'),
88        ('C:\\', 'C:\\'),
89    ])
90    def test_windows(self, directory, parent):
91        assert filescheme.parent_dir(directory) == parent
92
93    @pytest.mark.posix
94    @pytest.mark.parametrize('directory, parent', [
95        ('/home/foo', '/home'),
96        ('/home', '/'),
97        ('/home/', '/'),
98        ('/', '/'),
99    ])
100    def test_posix(self, directory, parent):
101        assert filescheme.parent_dir(directory) == parent
102
103
104def _file_url(path):
105    """Return a file:// url (as string) for the given LocalPath.
106
107    Arguments:
108        path: The filepath as LocalPath (as handled by py.path)
109    """
110    return urlutils.file_url(str(path))
111
112
113class TestDirbrowserHtml:
114
115    @dataclasses.dataclass
116    class Parsed:
117
118        parent: str
119        folders: List[str]
120        files: List[str]
121
122    @dataclasses.dataclass
123    class Item:
124
125        link: str
126        text: str
127
128    @pytest.fixture
129    def parser(self):
130        """Provide a function to get a parsed dirbrowser document."""
131        def parse(path):
132            html = filescheme.dirbrowser_html(path).decode('utf-8')
133            soup = bs4.BeautifulSoup(html, 'html.parser')
134
135            with testutils.ignore_bs4_warning():
136                print(soup.prettify())
137
138            container = soup('div', id='dirbrowserContainer')[0]
139
140            parent_elem = container('ul', class_='parent')
141            if not parent_elem:
142                parent = None
143            else:
144                parent = parent_elem[0].li.a.string
145
146            folders = []
147            files = []
148
149            for li in container('ul', class_='folders')[0]('li'):
150                item = self.Item(link=li.a['href'], text=str(li.a.string))
151                folders.append(item)
152
153            for li in container('ul', class_='files')[0]('li'):
154                item = self.Item(link=li.a['href'], text=str(li.a.string))
155                files.append(item)
156
157            return self.Parsed(parent=parent, folders=folders, files=files)
158
159        return parse
160
161    def test_basic(self):
162        html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8')
163        soup = bs4.BeautifulSoup(html, 'html.parser')
164
165        with testutils.ignore_bs4_warning():
166            print(soup.prettify())
167
168        container = soup.div
169        assert container['id'] == 'dirbrowserContainer'
170        title_elem = container('div', id='dirbrowserTitle')[0]
171        title_text = title_elem('p', id='dirbrowserTitleText')[0].text
172        assert title_text == 'Browse directory: {}'.format(os.getcwd())
173
174    def test_icons(self, monkeypatch):
175        """Make sure icon paths are correct file:// URLs."""
176        html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8')
177        soup = bs4.BeautifulSoup(html, 'html.parser')
178
179        with testutils.ignore_bs4_warning():
180            print(soup.prettify())
181
182        css = soup.html.head.style.string
183        assert "background-image: url('qute://resource/img/folder.svg');" in css
184
185    def test_empty(self, tmpdir, parser):
186        parsed = parser(str(tmpdir))
187        assert parsed.parent
188        assert not parsed.folders
189        assert not parsed.files
190
191    def test_files(self, tmpdir, parser):
192        foo_file = tmpdir / 'foo'
193        bar_file = tmpdir / 'bar'
194        foo_file.ensure()
195        bar_file.ensure()
196
197        parsed = parser(str(tmpdir))
198        assert parsed.parent
199        assert not parsed.folders
200        foo_item = self.Item(_file_url(foo_file), foo_file.relto(tmpdir))
201        bar_item = self.Item(_file_url(bar_file), bar_file.relto(tmpdir))
202        assert parsed.files == [bar_item, foo_item]
203
204    def test_html_special_chars(self, tmpdir, parser):
205        special_file = tmpdir / 'foo&bar'
206        special_file.ensure()
207
208        parsed = parser(str(tmpdir))
209        item = self.Item(_file_url(special_file), special_file.relto(tmpdir))
210        assert parsed.files == [item]
211
212    def test_dirs(self, tmpdir, parser):
213        foo_dir = tmpdir / 'foo'
214        bar_dir = tmpdir / 'bar'
215        foo_dir.ensure(dir=True)
216        bar_dir.ensure(dir=True)
217
218        parsed = parser(str(tmpdir))
219        assert parsed.parent
220        assert not parsed.files
221        foo_item = self.Item(_file_url(foo_dir), foo_dir.relto(tmpdir))
222        bar_item = self.Item(_file_url(bar_dir), bar_dir.relto(tmpdir))
223        assert parsed.folders == [bar_item, foo_item]
224
225    def test_mixed(self, tmpdir, parser):
226        foo_file = tmpdir / 'foo'
227        bar_dir = tmpdir / 'bar'
228        foo_file.ensure()
229        bar_dir.ensure(dir=True)
230
231        parsed = parser(str(tmpdir))
232        foo_item = self.Item(_file_url(foo_file), foo_file.relto(tmpdir))
233        bar_item = self.Item(_file_url(bar_dir), bar_dir.relto(tmpdir))
234        assert parsed.parent
235        assert parsed.files == [foo_item]
236        assert parsed.folders == [bar_item]
237
238    def test_root_dir(self, tmpdir, parser):
239        root_dir = 'C:\\' if utils.is_windows else '/'
240        parsed = parser(root_dir)
241        assert not parsed.parent
242
243    def test_oserror(self, mocker):
244        m = mocker.patch('qutebrowser.browser.webkit.network.filescheme.'
245                         'os.listdir')
246        m.side_effect = OSError('Error message')
247        html = filescheme.dirbrowser_html('').decode('utf-8')
248        soup = bs4.BeautifulSoup(html, 'html.parser')
249
250        with testutils.ignore_bs4_warning():
251            print(soup.prettify())
252
253        error_msg = soup('p', id='error-message-text')[0].string
254        assert error_msg == 'Error message'
255
256
257class TestFileSchemeHandler:
258
259    def test_dir(self, tmpdir):
260        url = QUrl.fromLocalFile(str(tmpdir))
261        req = QNetworkRequest(url)
262        reply = filescheme.handler(req, None, None)
263        # The URL will always use /, even on Windows - so we force this here
264        # too.
265        tmpdir_path = str(tmpdir).replace(os.sep, '/')
266        assert reply.readAll() == filescheme.dirbrowser_html(tmpdir_path)
267
268    def test_file(self, tmpdir):
269        filename = tmpdir / 'foo'
270        filename.ensure()
271        url = QUrl.fromLocalFile(str(filename))
272        req = QNetworkRequest(url)
273        reply = filescheme.handler(req, None, None)
274        assert reply is None
275
276    def test_unicode_encode_error(self, mocker):
277        url = QUrl('file:///tmp/foo')
278        req = QNetworkRequest(url)
279
280        err = UnicodeEncodeError('ascii', '', 0, 2, 'foo')
281        mocker.patch('os.path.isdir', side_effect=err)
282
283        reply = filescheme.handler(req, None, None)
284        assert reply is None
285