Repository: getnikola/nikola

Issue:
Possible bug in `get_asset_path`?
As mentioned in #2106, I think `nikola.util.get_asset_path` might have a bug, or it is used incorrectly. Or (third possiblity), I understand it wrongly what it is supposed to do.

I understand it as follows: if you want to know whether `output/bla/test.txt` is created by either the themes or by copying files (or already created by another plugin or by magic when called in `gen_tasks`), you do `get_asset_path('bla/test.txt', themes, files_folders)` and see if it returns `None` or a path.

I looked at how it is used for example in the `robots` plugin to detect whether `output/robots.txt` is created, and it seems to confirm my idea.

If that's correct, then it will fail in the following situation: assume that you have `file_folders = {'a': 'b'}` and have a file `a/bla/test.txt`. This will be copied to `output/b/bla/test.txt`. On the other hand, `get_asset_path('bla/test.txt', themes, files_folders)` will return `a/bla/test.txt`, so I assume that `output/bla/test.txt` will be created -- which it won't.

So -- do I understand it wrong what `get_asset_path` does, or is there a bug in its implementation? (Or am I somehow else confused? :) )



Discussion:
Direct link to `get_asset_path`: https://github.com/getnikola/nikola/blob/master/nikola/utils.py#L971

The last argument, which was added recently, is sort of broken by default.

The idea is:

1) You want to know where something comes from? Call get_asset_path with output_dir=None. That way you will know if it came from a theme or from files/ 

2) You want to know if a file is generated at all, then use output_dir = output/ (or whatever) and if a previous task on which you depend already has created it, you will get it. 

The case for 2) is because CSS bundles need to depend on files created by the SASS compiler, which creates them in output/ directly.

So, in general, you are in case 1) and bundles is the only usage I know of case 2). Thus the default for output_dir should really be None, not 'output'.

I forgot: get_asset_path is meant to be used FOR ASSETS. That means "things that usually would come from a theme or some random CSS the user threw in files/" and not as a general "where files come from".

Give what you say @ralsina, it’s use in robots.py is completely wrong.

Well, it looks correct to me, it's like my case 1) but without themes, so just "is robots.txt coming from files/?" ... maybe that's a case "3" :-)

But it isn't checking if `robots.txt` is coming from files. It is checking whether the files folders contain such a file, but it completely ignores the possibility that these files are copied somewhere completely different.

Erm... no?

```
    for src, rel_dst in files_folders.items():
        candidate = os.path.abspath(os.path.join(src, path))
        if os.path.isfile(candidate):
            return candidate
```

If `rel_dst` is not empty, the resulting file will be copied somewhere else.

In which case this is a bug in get_asset_path, not in how it's used :-)

That was my initial question :) The documentation isn't exactly clear on whether this is a bug or intended behavior.
A correct implementation (I think) should check if `path` starts with `rel_dest`, and if it does, remove that beginning from `path` before joining with `src`. Of course, this must be done with different path separators in mind.

I don't have the time this week to work on Nikola, but something like

```
candidate = os.path.join(src, os.path.relpath(path, rel_dst))
```

is more or less what we want.

Together with a check that the result of `os.path.relpath` doesn't begin with `/` or `..` or `c:\` or ... :)


Resolving patch:
diff --git a/nikola/utils.py b/nikola/utils.py
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -986,9 +986,12 @@ def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='theme
     >>> print(get_asset_path('nikola.py', ['bootstrap3', 'base'], {'nikola': ''}))
     /.../nikola/nikola.py
 
-    >>> print(get_asset_path('nikola/nikola.py', ['bootstrap3', 'base'], {'nikola':'nikola'}))
+    >>> print(get_asset_path('nikola.py', ['bootstrap3', 'base'], {'nikola': 'nikola'}))
     None
 
+    >>> print(get_asset_path('nikola/nikola.py', ['bootstrap3', 'base'], {'nikola': 'nikola'}))
+    /.../nikola/nikola.py
+
     """
     for theme_name in themes:
         candidate = os.path.join(
@@ -998,9 +1001,11 @@ def get_asset_path(path, themes, files_folders={'files': ''}, _themes_dir='theme
         if os.path.isfile(candidate):
             return candidate
     for src, rel_dst in files_folders.items():
-        candidate = os.path.abspath(os.path.join(src, path))
-        if os.path.isfile(candidate):
-            return candidate
+        relpath = os.path.normpath(os.path.relpath(path, rel_dst))
+        if not relpath.startswith('..' + os.path.sep):
+            candidate = os.path.abspath(os.path.join(src, relpath))
+            if os.path.isfile(candidate):
+                return candidate
 
     if output_dir:
         candidate = os.path.join(output_dir, path)


Test patch: