Repository: ytdl-org/youtube-dl

Issue:
Option --write-info-json for video with percent sign in title causes TypeError "not enough arguments for format string"
## Checklist

- [x] I'm reporting a broken site support issue
- [x] I've verified that I'm running youtube-dl version **2021.12.17**
- [x] I've checked that all provided URLs are alive and playable in a browser
- [x] I've checked that all URLs and arguments with special characters are properly quoted or escaped
- [x] I've searched the bugtracker for similar bug reports including closed ones
- [ ] I've read bugs section in FAQ
  - because it's currently unavailable, blocked due to court decision


## Verbose log

```
[debug] System config: []
[debug] User config: []
[debug] Custom config: []
[debug] Command-line args: ['--no-call-home', '--abort-on-error', '--no-overwrites', '--keep-video', '--fixup=warn', '--restrict-filenames', '--output', '%(upload_date)s.%(title)s.yt-%(id)s.%(ext)s', '--prefer-ffmpeg', '--format=18', '--verbose', '--', 'http://youtu.be/bwvAk9W2GkM']
[debug] Encodings: locale UTF-8, fs utf-8, out utf-8, pref UTF-8
[debug] youtube-dl version 2021.12.17
[debug] Git HEAD: 66ab0814c
[debug] Python 3.8.10 (CPython x86_64 64bit) - Linux-5.15.0-76-lowlatency-x86_64-with-glibc2.29 - OpenSSL 1.1.1f  31 Mar 2020 - glibc 2.31
[debug] exe versions: ffmpeg 4.2.7, ffprobe 4.2.7
[debug] Proxy map: █████
[youtube] bwvAk9W2GkM: Downloading webpage
[youtube] bwvAk9W2GkM: Downloading player afd1b6e5
[debug] [youtube] Decrypted nsig █████ => █████
[debug] [youtube] Decrypted nsig █████ => █████
[youtube] bwvAk9W2GkM: Downloading API JSON
Traceback (most recent call last):
  File "/…/rg3wrap.py", line 79, in <module>
    youtube_dl._real_main(
  File "/…/ytdl-rg3/youtube_dl/__init__.py", line 463, in _real_main
    retcode = ydl.download(all_urls)
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 2218, in download
    res = self.extract_info(
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 852, in extract_info
    return self.__extract_info(url, ie, download, extra_info, process)
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 859, in wrapper
    return func(self, *args, **kwargs)
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 966, in __extract_info
    return self.process_ie_result(ie_result, download, extra_info)
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 1000, in process_ie_result
    return self.process_video_result(ie_result, download=download)
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 1835, in process_video_result
    self.process_info(new_info)
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 2035, in process_info
    self._write_info_json(
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 2638, in _write_info_json
    self.to_screen(msg('[info] Writing %s as JSON to: ' + infofn, label))
  File "/…/ytdl-rg3/youtube_dl/YoutubeDL.py", line 2629, in msg
    return fmt % (lbl + ' metadata',)
TypeError: not enough arguments for format string
```


## Description

Looks like a format string injection vulnerability but with impact limited to minor annoyance for people who use the video title in their filename. My workaround is to use `--output '%(upload_date)s.FIXME.yt-%(id)s.%(ext)s'` for failing videos and renaming them manually.

I searched and found a video whose title contains 1. percent sign followed by space and 2. percent sign followed by letter, so we can test both cases in one go. First is popular in "100%" titles, 2nd because a valid sequence may trigger bugs that would be skipped by the first case. Unsurprisingly, first result was: [String Formatting in Python using % | What's difference between %s & %d in Python string | Format()](https://vid.puffyan.us/watch?v=bwvAk9W2GkM)


Discussion:

Resolving patch:
diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py
--- a/youtube_dl/YoutubeDL.py
+++ b/youtube_dl/YoutubeDL.py
@@ -2635,12 +2635,12 @@ def msg(fmt, lbl):
             self.to_screen(msg('[info] %s is already present', label.title()))
             return 'exists'
         else:
-            self.to_screen(msg('[info] Writing %s as JSON to: ' + infofn, label))
+            self.to_screen(msg('[info] Writing %s as JSON to: ', label) + infofn)
             try:
                 write_json_file(self.filter_requested_info(info_dict), infofn)
                 return True
             except (OSError, IOError):
-                self.report_error(msg('Cannot write %s to JSON file ' + infofn, label))
+                self.report_error(msg('Cannot write %s to JSON file ', label) + infofn)
                 return
 
     def _write_thumbnails(self, info_dict, filename):


Test patch: