kdk 3.2.0__tar.gz → 3.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {kdk-3.2.0/src/kdk.egg-info → kdk-3.3.1}/PKG-INFO +3 -3
  2. {kdk-3.2.0 → kdk-3.3.1}/README.md +2 -2
  3. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/__init__.py +1 -1
  4. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/piers/strings.po +64 -3
  5. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/skin.py +79 -30
  6. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/urls.py +29 -3
  7. {kdk-3.2.0 → kdk-3.3.1/src/kdk.egg-info}/PKG-INFO +3 -3
  8. {kdk-3.2.0 → kdk-3.3.1}/tests/test_urls.py +84 -18
  9. {kdk-3.2.0 → kdk-3.3.1}/LICENSE +0 -0
  10. {kdk-3.2.0 → kdk-3.3.1}/pyproject.toml +0 -0
  11. {kdk-3.2.0 → kdk-3.3.1}/setup.cfg +0 -0
  12. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/__main__.py +0 -0
  13. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/config.py +0 -0
  14. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/core.py +0 -0
  15. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/addon_schema/addon.xsd +0 -0
  16. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/addon_schema/gotham_skin.xsd +0 -0
  17. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/addon_schema/matrix_metadata.xsd +0 -0
  18. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/icon.ico +0 -0
  19. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/omega/colors.xml +0 -0
  20. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/omega/strings.po +0 -0
  21. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/piers/colors.xml +0 -0
  22. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi_builtin_controls.xml +0 -0
  23. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/controls.xml +0 -0
  24. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/data.xml +0 -0
  25. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/infonames.json +0 -0
  26. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/obsolete_windows.json +0 -0
  27. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/windows.json +0 -0
  28. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/controls.xml +0 -0
  29. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/data.xml +0 -0
  30. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/infonames.json +0 -0
  31. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/obsolete_windows.json +0 -0
  32. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/windows.json +0 -0
  33. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/gui/__init__.py +0 -0
  34. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/gui/__main__.py +0 -0
  35. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/gui/app.py +0 -0
  36. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/__init__.py +0 -0
  37. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/addon/__init__.py +0 -0
  38. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/addon/addon.py +0 -0
  39. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/addon/translations.py +0 -0
  40. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/coredata.py +0 -0
  41. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infonames.py +0 -0
  42. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/__init__.py +0 -0
  43. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/checker.py +0 -0
  44. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/loader.py +0 -0
  45. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/provider.py +0 -0
  46. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/polib/LICENSE +0 -0
  47. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/polib/__init__.py +0 -0
  48. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/polib/polib.py +0 -0
  49. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/reporting/__init__.py +0 -0
  50. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/reporting/text.py +0 -0
  51. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/__init__.py +0 -0
  52. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/include.py +0 -0
  53. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/index.py +0 -0
  54. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/maps.py +0 -0
  55. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/resolution.py +0 -0
  56. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/resources.py +0 -0
  57. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/textures.py +0 -0
  58. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/__init__.py +0 -0
  59. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/colors.py +0 -0
  60. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/expressions.py +0 -0
  61. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/files.py +0 -0
  62. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/images.py +0 -0
  63. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/infobool.py +0 -0
  64. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/validation.py +0 -0
  65. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/xml.py +0 -0
  66. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/__init__.py +0 -0
  67. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/constants.py +0 -0
  68. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/filecheck.py +0 -0
  69. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/font.py +0 -0
  70. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/hierarchy.py +0 -0
  71. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/ids.py +0 -0
  72. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/image.py +0 -0
  73. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/include.py +0 -0
  74. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/interpreter.py +0 -0
  75. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/label.py +0 -0
  76. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/repo.py +0 -0
  77. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/rules.py +0 -0
  78. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/skinmap.py +0 -0
  79. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/suppress.py +0 -0
  80. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/variable.py +0 -0
  81. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/__init__.py +0 -0
  82. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/browse.py +0 -0
  83. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/cache.py +0 -0
  84. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/compare.py +0 -0
  85. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/logs.py +0 -0
  86. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/payload.py +0 -0
  87. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/progress.py +0 -0
  88. {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/render.py +0 -0
  89. {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/SOURCES.txt +0 -0
  90. {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/dependency_links.txt +0 -0
  91. {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/entry_points.txt +0 -0
  92. {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/requires.txt +0 -0
  93. {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/top_level.txt +0 -0
  94. {kdk-3.2.0 → kdk-3.3.1}/tests/test_addon_type.py +0 -0
  95. {kdk-3.2.0 → kdk-3.3.1}/tests/test_archive.py +0 -0
  96. {kdk-3.2.0 → kdk-3.3.1}/tests/test_bracket_validation.py +0 -0
  97. {kdk-3.2.0 → kdk-3.3.1}/tests/test_brackets.py +0 -0
  98. {kdk-3.2.0 → kdk-3.3.1}/tests/test_circular_references.py +0 -0
  99. {kdk-3.2.0 → kdk-3.3.1}/tests/test_cli.py +0 -0
  100. {kdk-3.2.0 → kdk-3.3.1}/tests/test_color_values.py +0 -0
  101. {kdk-3.2.0 → kdk-3.3.1}/tests/test_compare.py +0 -0
  102. {kdk-3.2.0 → kdk-3.3.1}/tests/test_conditional_defaults.py +0 -0
  103. {kdk-3.2.0 → kdk-3.3.1}/tests/test_constant_resolution_accuracy.py +0 -0
  104. {kdk-3.2.0 → kdk-3.3.1}/tests/test_control_template_semantics.py +0 -0
  105. {kdk-3.2.0 → kdk-3.3.1}/tests/test_coredata.py +0 -0
  106. {kdk-3.2.0 → kdk-3.3.1}/tests/test_expression_resolution.py +0 -0
  107. {kdk-3.2.0 → kdk-3.3.1}/tests/test_expressions.py +0 -0
  108. {kdk-3.2.0 → kdk-3.3.1}/tests/test_extract_expression_at_offset.py +0 -0
  109. {kdk-3.2.0 → kdk-3.3.1}/tests/test_file_integrity.py +0 -0
  110. {kdk-3.2.0 → kdk-3.3.1}/tests/test_file_name_case.py +0 -0
  111. {kdk-3.2.0 → kdk-3.3.1}/tests/test_font_validation.py +0 -0
  112. {kdk-3.2.0 → kdk-3.3.1}/tests/test_github_output.py +0 -0
  113. {kdk-3.2.0 → kdk-3.3.1}/tests/test_hierarchy.py +0 -0
  114. {kdk-3.2.0 → kdk-3.3.1}/tests/test_id_refs.py +0 -0
  115. {kdk-3.2.0 → kdk-3.3.1}/tests/test_ids.py +0 -0
  116. {kdk-3.2.0 → kdk-3.3.1}/tests/test_image_info.py +0 -0
  117. {kdk-3.2.0 → kdk-3.3.1}/tests/test_image_validation.py +0 -0
  118. {kdk-3.2.0 → kdk-3.3.1}/tests/test_include_faults.py +0 -0
  119. {kdk-3.2.0 → kdk-3.3.1}/tests/test_include_file_refs.py +0 -0
  120. {kdk-3.2.0 → kdk-3.3.1}/tests/test_include_validation.py +0 -0
  121. {kdk-3.2.0 → kdk-3.3.1}/tests/test_includes.py +0 -0
  122. {kdk-3.2.0 → kdk-3.3.1}/tests/test_index_order.py +0 -0
  123. {kdk-3.2.0 → kdk-3.3.1}/tests/test_infobool.py +0 -0
  124. {kdk-3.2.0 → kdk-3.3.1}/tests/test_infonames.py +0 -0
  125. {kdk-3.2.0 → kdk-3.3.1}/tests/test_interpreter.py +0 -0
  126. {kdk-3.2.0 → kdk-3.3.1}/tests/test_json_output.py +0 -0
  127. {kdk-3.2.0 → kdk-3.3.1}/tests/test_kodi_resolution.py +0 -0
  128. {kdk-3.2.0 → kdk-3.3.1}/tests/test_label_attrs.py +0 -0
  129. {kdk-3.2.0 → kdk-3.3.1}/tests/test_label_validation.py +0 -0
  130. {kdk-3.2.0 → kdk-3.3.1}/tests/test_load_order.py +0 -0
  131. {kdk-3.2.0 → kdk-3.3.1}/tests/test_maps.py +0 -0
  132. {kdk-3.2.0 → kdk-3.3.1}/tests/test_muted_output.py +0 -0
  133. {kdk-3.2.0 → kdk-3.3.1}/tests/test_obsolete_windows.py +0 -0
  134. {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_in_include_names.py +0 -0
  135. {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_resolution.py +0 -0
  136. {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_value.py +0 -0
  137. {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_values.py +0 -0
  138. {kdk-3.2.0 → kdk-3.3.1}/tests/test_parse_errors.py +0 -0
  139. {kdk-3.2.0 → kdk-3.3.1}/tests/test_po_strings.py +0 -0
  140. {kdk-3.2.0 → kdk-3.3.1}/tests/test_release_gated_tags.py +0 -0
  141. {kdk-3.2.0 → kdk-3.3.1}/tests/test_repo.py +0 -0
  142. {kdk-3.2.0 → kdk-3.3.1}/tests/test_rules.py +0 -0
  143. {kdk-3.2.0 → kdk-3.3.1}/tests/test_save_report.py +0 -0
  144. {kdk-3.2.0 → kdk-3.3.1}/tests/test_singleton_tags.py +0 -0
  145. {kdk-3.2.0 → kdk-3.3.1}/tests/test_skin_string_range.py +0 -0
  146. {kdk-3.2.0 → kdk-3.3.1}/tests/test_split_top_level_commas.py +0 -0
  147. {kdk-3.2.0 → kdk-3.3.1}/tests/test_suppress.py +0 -0
  148. {kdk-3.2.0 → kdk-3.3.1}/tests/test_textures.py +0 -0
  149. {kdk-3.2.0 → kdk-3.3.1}/tests/test_utils.py +0 -0
  150. {kdk-3.2.0 → kdk-3.3.1}/tests/test_valueless_tags.py +0 -0
  151. {kdk-3.2.0 → kdk-3.3.1}/tests/test_variables.py +0 -0
  152. {kdk-3.2.0 → kdk-3.3.1}/tests/test_xbt.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kdk
3
- Version: 3.2.0
3
+ Version: 3.3.1
4
4
  Summary: Kodi skin validation tool - CLI and GUI
5
5
  Author-email: MikeSiLVO <mikesilvo164@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -54,7 +54,7 @@ contain the same code; the GUI build runs without a console window, so
54
54
  double-clicking it doesn't open a terminal. No Python install needed.
55
55
 
56
56
  Download them from the [Releases page](https://github.com/MikeSiLVO/kdk/releases).
57
- `<version>` is the release tag, such as `v3.2.0`:
57
+ `<version>` is the release tag, such as `v3.3.1`:
58
58
 
59
59
  - **Windows (x86_64)**: `kdk-gui-<version>-windows-x86_64.exe` and
60
60
  `kdk-cli-<version>-windows-x86_64.exe`.
@@ -196,7 +196,7 @@ still points at the right file.
196
196
  Run it on `ubuntu-latest`. Windows runners check text files out with CRLF line
197
197
  endings, which File Integrity reports as errors.
198
198
 
199
- ## Silencing a finding
199
+ ## Muting a finding
200
200
 
201
201
  An XML comment mutes warnings the skin has already judged, the way a
202
202
  `# type: ignore` does in Python. `kdk-ignore-file` covers the whole file it sits
@@ -15,7 +15,7 @@ contain the same code; the GUI build runs without a console window, so
15
15
  double-clicking it doesn't open a terminal. No Python install needed.
16
16
 
17
17
  Download them from the [Releases page](https://github.com/MikeSiLVO/kdk/releases).
18
- `<version>` is the release tag, such as `v3.2.0`:
18
+ `<version>` is the release tag, such as `v3.3.1`:
19
19
 
20
20
  - **Windows (x86_64)**: `kdk-gui-<version>-windows-x86_64.exe` and
21
21
  `kdk-cli-<version>-windows-x86_64.exe`.
@@ -157,7 +157,7 @@ still points at the right file.
157
157
  Run it on `ubuntu-latest`. Windows runners check text files out with CRLF line
158
158
  endings, which File Integrity reports as errors.
159
159
 
160
- ## Silencing a finding
160
+ ## Muting a finding
161
161
 
162
162
  An XML comment mutes warnings the skin has already judged, the way a
163
163
  `# type: ignore` does in Python. `kdk-ignore-file` covers the whole file it sits
@@ -1,3 +1,3 @@
1
1
  """KDK - Kodi skin validation tool."""
2
2
 
3
- __version__ = "3.2.0"
3
+ __version__ = "3.3.1"
@@ -9537,7 +9537,13 @@ msgctxt "#16297"
9537
9537
  msgid "Smooth"
9538
9538
  msgstr ""
9539
9539
 
9540
- #empty strings from id 16298 to 16299
9540
+ #. Name of the video filter category that holds the filters without a shader
9541
+ #: xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp
9542
+ msgctxt "#16298"
9543
+ msgid "Built-in"
9544
+ msgstr ""
9545
+
9546
+ #empty string with id 16299
9541
9547
 
9542
9548
  #: xbmc/video/dialogs/GUIDialogVideoSettings.cpp
9543
9549
  msgctxt "#16300"
@@ -19635,7 +19641,13 @@ msgctxt "#35309"
19635
19641
  msgid "Rewind"
19636
19642
  msgstr ""
19637
19643
 
19638
- #empty strings from id 35310 to 35319
19644
+ #. Label of the Manual button in the game OSD, which shows the game's instruction booklet
19645
+ #: addons/skin.estuary/xml/GameOSD.xml
19646
+ msgctxt "#35310"
19647
+ msgid "Manual"
19648
+ msgstr ""
19649
+
19650
+ #empty strings from id 35311 to 35319
19639
19651
 
19640
19652
  #. Label of the Cheats button in the game OSD, heading of the cheats dialog, and
19641
19653
  #. of the cheats group in settings
@@ -20002,7 +20014,55 @@ msgctxt "#35703"
20002
20014
  msgid "Hardcore mode: {0:d} of {1:d} achievements unlocked"
20003
20015
  msgstr ""
20004
20016
 
20005
- #empty strings from id 35704 to 35999
20017
+ #empty strings from id 35704 to 35709
20018
+
20019
+ #. Marks a locked achievement that can be missed for good
20020
+ #: addons/skin.estuary/xml/Includes_Games.xml
20021
+ msgctxt "#35710"
20022
+ msgid "Missable"
20023
+ msgstr ""
20024
+
20025
+ #. Button that picks which of a game's achievement sets is shown
20026
+ #: addons/skin.estuary/xml/Includes_Games.xml
20027
+ msgctxt "#35711"
20028
+ msgid "Achievement set"
20029
+ msgstr ""
20030
+
20031
+ #. An achievement set whose progression and win achievements are all earned
20032
+ #: xbmc/games/addons/cheevos/GameClientCheevos.cpp
20033
+ #: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
20034
+ msgctxt "#35712"
20035
+ msgid "Beaten"
20036
+ msgstr ""
20037
+
20038
+ #. How close an achievement set is to being beaten, {0:d} earned of {1:d} needed
20039
+ #: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
20040
+ #, c++-format
20041
+ msgctxt "#35713"
20042
+ msgid "{0:d} of {1:d} to beat"
20043
+ msgstr ""
20044
+
20045
+ #. An achievement set the player turned off
20046
+ #: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
20047
+ msgctxt "#35714"
20048
+ msgid "Turned off"
20049
+ msgstr ""
20050
+
20051
+ #. The game's own achievement set, as opposed to its subsets
20052
+ #: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
20053
+ msgctxt "#35715"
20054
+ msgid "Main Set"
20055
+ msgstr ""
20056
+
20057
+ #empty strings from id 35716 to 35717
20058
+
20059
+ #. Marks an achievement being attempted in the list of a game's achievements
20060
+ #: addons/skin.estuary/xml/Includes_Games.xml
20061
+ msgctxt "#35718"
20062
+ msgid "Attempt active"
20063
+ msgstr ""
20064
+
20065
+ #empty strings from id 35719 to 35999
20006
20066
 
20007
20067
  #: xbmc/peripherals/devices/PeripheralCecAdapter.cpp
20008
20068
  msgctxt "#36000"
@@ -20217,6 +20277,7 @@ msgstr ""
20217
20277
  #: xbmc/video/VideoItemArtworkHandler.cpp
20218
20278
  #: xbmc/video/dialogs/GUIDialogVideoInfo.cpp
20219
20279
  #: xbmc/music/dialogs/GUIDialogMusicInfo.cpp
20280
+ #: xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp
20220
20281
  msgctxt "#36041"
20221
20282
  msgid "* Item folder"
20222
20283
  msgstr ""
@@ -7,6 +7,7 @@ import re
7
7
  import time
8
8
  from pathlib import Path
9
9
 
10
+ from ..utils.expressions import resolve_params_in_text
10
11
  from ..utils.files import check_paths, is_inside
11
12
  from ..utils.infobool import condition_states
12
13
  from ..utils.xml import get_params, get_root_from_file
@@ -30,6 +31,35 @@ def _same_definition(a, b):
30
31
  return b is not None and (a.text or "").strip() == (b.text or "").strip() and dict(a.attrib) == dict(b.attrib)
31
32
 
32
33
 
34
+ _PARAM_REF = re.compile(r"\$PARAM\[([^,\]]+)")
35
+
36
+
37
+ def _param_uses(include_node):
38
+ """Yield (param, use) per $PARAM in the spliced body; forwards flag a bare $PARAM value."""
39
+ definition = include_node.find("definition")
40
+ body = definition if definition is not None else include_node
41
+ for node in body.iterdescendants():
42
+ if not isinstance(node.tag, str):
43
+ continue
44
+ parent = node.getparent()
45
+ target = parent.get("content", "") if parent.tag == "include" else ""
46
+ forward = node.get("name") if node.tag == "param" and target and "$" not in target else None
47
+ texts = [(None, node.text or "")]
48
+ texts += [(a, v) for a, v in node.attrib.items() if not a.startswith("_kdk_")]
49
+ for attr, value in texts:
50
+ if "$PARAM[" not in value:
51
+ continue
52
+ if forward and attr in (None, "value"):
53
+ use = ("forward", target, forward,
54
+ resolve_params_in_text(value)[1] == "SINGLE_UNDEFINED")
55
+ elif attr is None:
56
+ use = ("tag", node.tag)
57
+ else:
58
+ use = ("attr", attr)
59
+ for match in _PARAM_REF.finditer(value):
60
+ yield match.group(1).strip(), use
61
+
62
+
33
63
  class Skin(Addon):
34
64
  """A Kodi skin: includes, colors, fonts, and resolved windows."""
35
65
 
@@ -109,9 +139,8 @@ class Skin(Addon):
109
139
  return self._resource_loader
110
140
 
111
141
  def _build_include_param_roles(self):
112
- """Role of each $PARAM per include body: tag, attribute, forward to a nested call, multi."""
142
+ """Build each include body's $PARAM roles: tag, attribute, forward to nested call, multi."""
113
143
  result = {}
114
- param_re = re.compile(r"\$PARAM\[([^,\]]+)")
115
144
 
116
145
  def mark(roles, pname, new_role):
117
146
  existing = roles.get(pname)
@@ -122,38 +151,12 @@ class Skin(Addon):
122
151
  else:
123
152
  roles[pname] = "multi"
124
153
 
125
- def role_for(node):
126
- parent = node.getparent() if hasattr(node, "getparent") else None
127
- if (node.tag == "param" and parent is not None
128
- and parent.tag == "include" and parent.get("content")):
129
- target_inc = parent.get("content")
130
- target_param = node.get("name") or ""
131
- if target_inc and target_param:
132
- return ("forward", target_inc, target_param)
133
- return None
134
-
135
154
  for folder, includes_for_folder in self.include_map.items():
136
155
  folder_result = {}
137
156
  for inc_name, (inc_node, _defaults, _file) in includes_for_folder.items():
138
157
  roles: dict = {}
139
- for node in inc_node.iter():
140
- if node is inc_node:
141
- continue
142
- parent = node.getparent() if hasattr(node, "getparent") else None
143
- if (node.tag == "param" and parent is inc_node):
144
- continue
145
- text = (node.text or "")
146
- if "$PARAM[" in text:
147
- forward_role = role_for(node)
148
- for match in param_re.finditer(text):
149
- pname = match.group(1).strip()
150
- mark(roles, pname, forward_role or ("tag", node.tag))
151
- for attr_name, attr_value in node.attrib.items():
152
- if attr_name.startswith("_kdk_") or "$PARAM[" not in attr_value:
153
- continue
154
- for match in param_re.finditer(attr_value):
155
- pname = match.group(1).strip()
156
- mark(roles, pname, ("attr", attr_name))
158
+ for pname, use in _param_uses(inc_node):
159
+ mark(roles, pname, use[:3])
157
160
  if roles:
158
161
  folder_result[inc_name] = roles
159
162
  if folder_result:
@@ -189,6 +192,52 @@ class Skin(Addon):
189
192
 
190
193
  return role
191
194
 
195
+ def include_params(self, folder, include_name):
196
+ """Each param the include declares or uses, as (name, default, used directly, forwards)."""
197
+ entry = self.include_map.get(folder, {}).get(include_name)
198
+ if entry is None:
199
+ return []
200
+ node, defaults, _file = entry
201
+ uses: dict[str, list] = {}
202
+ for param, use in _param_uses(node):
203
+ uses.setdefault(param, []).append(use)
204
+ for param in defaults:
205
+ uses.setdefault(param, [])
206
+ rows = []
207
+ for param, found in uses.items():
208
+ default = defaults.get(param)
209
+ forwards = []
210
+ for use in found:
211
+ if use[0] != "forward":
212
+ continue
213
+ _, target, target_param, alone = use
214
+ # what the target gets when a call leaves the param out; a default passes itself
215
+ value = None
216
+ if default is None and alone:
217
+ value = self._omitted_value(folder, target, target_param, {(include_name, param)})
218
+ if (target, target_param, value) not in forwards:
219
+ forwards.append((target, target_param, value))
220
+ direct = any(use[0] != "forward" for use in found)
221
+ rows.append((param, default, direct, forwards))
222
+ return rows
223
+
224
+ def _omitted_value(self, folder, include_name, param, seen):
225
+ """Value a param takes when a call omits it; None unless one default reaches every use."""
226
+ entry = self.include_map.get(folder, {}).get(include_name)
227
+ if entry is None or (include_name, param) in seen:
228
+ return None
229
+ node, defaults, _file = entry
230
+ if param in defaults:
231
+ return defaults[param]
232
+ values = set()
233
+ for found, use in _param_uses(node):
234
+ if found != param:
235
+ continue
236
+ if use[0] != "forward" or not use[3]:
237
+ return None
238
+ values.add(self._omitted_value(folder, use[1], use[2], seen | {(include_name, param)}))
239
+ return values.pop() if len(values) == 1 else None
240
+
192
241
  def _load_colors_and_fonts(self):
193
242
  """Load colors and fonts through the resource reader, dropping the validation index."""
194
243
  from ..coredata import kodi_colors_xml
@@ -11,7 +11,7 @@ _VALUE_KEY = re.compile(r'(?:%22|")value(?:%22|")\s*(?:%3A|:)', re.IGNORECASE)
11
11
  _VALUE_END = re.compile(r'%5D|%7D|[\]}]', re.IGNORECASE)
12
12
  _EXPRESSION = re.compile(r'\$(ESC)?(INFO|VAR)\[')
13
13
  _PLUGIN_QUERY = re.compile(r'plugin://[^\s"\'?]*\?', re.IGNORECASE)
14
- _URL_END = re.compile(r'[\s"\',)]')
14
+ _URL_END = re.compile(r'["\',)]')
15
15
  # an '&' that starts no key=value pair, or a '|', inside a value
16
16
  _DATA_SEPARATOR = re.compile(r'&(?![\w.-]+=)|\|')
17
17
  # the key of the last pair a text opens, as in ?key= or &key=
@@ -23,6 +23,10 @@ _ENCODER = re.compile(r'get_encoded_string\b.*?\bwindow_prop=([^&"\s,)]+)', re.I
23
23
  _PROPERTY = re.compile(r'window(?:\([^)]*\))?\.property\((.+)\)$', re.IGNORECASE)
24
24
  _SKIN_STRING = re.compile(r'skin\.string\((.+)\)$', re.IGNORECASE)
25
25
  _ITEM_PROPERTY = re.compile(r'container\((\d+)\)\.listitem\w*(?:\([^)]*\))?\.property\((.+)\)$', re.IGNORECASE)
26
+ _ITEM_LABEL = re.compile(r'container\((\d+)\)\.listitem\w*(?:\([^)]*\))?\.label$', re.IGNORECASE)
27
+ # script.skinvariables fills a list with one item per && path, its label URL-encoded
28
+ _ENCODER_LIST = re.compile(r'\binfo=get_encoded_string\b', re.IGNORECASE)
29
+ _ONLY_VAR = re.compile(r'\s*\$VAR\[([^\]]+)\]\s*$')
26
30
  # an edit control's text as typed; no other control answers Index(1)
27
31
  _TYPED = re.compile(r'control\.getlabel\(\s*\d+\s*\)\.index\(\s*1\s*\)$', re.IGNORECASE)
28
32
  # characters a URL query or a JSON string cannot carry unencoded
@@ -47,7 +51,7 @@ def _plain(name):
47
51
 
48
52
 
49
53
  def _url_end(text, pos):
50
- """Index where the URL starting before the position ends: a space, quote, comma or ')'."""
54
+ """Index where the URL starting before the position ends: a quote, comma or ')'."""
51
55
  found = _URL_END.search(text, pos)
52
56
  return found.start() if found else len(text)
53
57
 
@@ -58,7 +62,7 @@ def _worst(kinds):
58
62
 
59
63
 
60
64
  def value_sources(paths):
61
- """Each value the skin stores per property, skin string and static item property, by name."""
65
+ """Each value the skin stores per property, skin string, static item property and list id."""
62
66
  sources = {}
63
67
  for path in paths:
64
68
  root = get_root_from_file(path)
@@ -72,6 +76,11 @@ def value_sources(paths):
72
76
  if control is not None and (control.get("id") or "").isdigit():
73
77
  key = f"item:{control.get('id')}:{element.get('name').casefold()}"
74
78
  sources.setdefault(key, []).append(element.text or "")
79
+ parent = element.getparent()
80
+ if element.tag == "content" and parent is not None and parent.tag == "control":
81
+ control_id = parent.get("id") or ""
82
+ if control_id.isdigit():
83
+ sources.setdefault(f"content:{control_id}", []).append((element.text or "").strip())
75
84
  for text in [element.text or ""] + list(element.attrib.values()):
76
85
  for name, args in builtin_calls(text, ("SetProperty", "Skin.SetString")):
77
86
  if not args or "$" in args[0]:
@@ -171,8 +180,25 @@ class UrlValues:
171
180
  return SAFE if _plain(found.group(1)) else UNTRACED
172
181
  if _TYPED.search(info):
173
182
  return TYPED
183
+ found = _ITEM_LABEL.search(info)
184
+ if found and self._encoder_list(found.group(1)):
185
+ return SAFE
174
186
  return SAFE if _plain(info.rsplit(".", 1)[-1]) else RAW
175
187
 
188
+ def _encoder_list(self, control_id):
189
+ """True when lists with the id exist and the encoder fills each one."""
190
+ paths = self.sources.get(f"content:{control_id}")
191
+ return bool(paths) and all(self._encoder_path(path, frozenset()) for path in paths)
192
+
193
+ def _encoder_path(self, text, seen):
194
+ """True when a list path calls the encoder, or is only a $VAR whose every value does."""
195
+ if _ENCODER_LIST.search(text):
196
+ return True
197
+ found = _ONLY_VAR.match(text)
198
+ name = found.group(1).strip() if found else ""
199
+ values = self.variables.get(name) if name not in seen else None
200
+ return bool(values) and all(self._encoder_path(value, seen | {name}) for value in values)
201
+
176
202
  def _stored_kind(self, key, seen):
177
203
  """Kind of a property, skin string or static item property, from every value it holds."""
178
204
  if key in seen:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kdk
3
- Version: 3.2.0
3
+ Version: 3.3.1
4
4
  Summary: Kodi skin validation tool - CLI and GUI
5
5
  Author-email: MikeSiLVO <mikesilvo164@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -54,7 +54,7 @@ contain the same code; the GUI build runs without a console window, so
54
54
  double-clicking it doesn't open a terminal. No Python install needed.
55
55
 
56
56
  Download them from the [Releases page](https://github.com/MikeSiLVO/kdk/releases).
57
- `<version>` is the release tag, such as `v3.2.0`:
57
+ `<version>` is the release tag, such as `v3.3.1`:
58
58
 
59
59
  - **Windows (x86_64)**: `kdk-gui-<version>-windows-x86_64.exe` and
60
60
  `kdk-cli-<version>-windows-x86_64.exe`.
@@ -196,7 +196,7 @@ still points at the right file.
196
196
  Run it on `ubuntu-latest`. Windows runners check text files out with CRLF line
197
197
  endings, which File Integrity reports as errors.
198
198
 
199
- ## Silencing a finding
199
+ ## Muting a finding
200
200
 
201
201
  An XML comment mutes warnings the skin has already judged, the way a
202
202
  `# type: ignore` does in Python. `kdk-ignore-file` covers the whole file it sits
@@ -16,11 +16,35 @@ PATH = "videodb://movies/titles/?xsp=%7B%22rules%22%3A%7B%22and%22%3A%5B%7B%22fi
16
16
  "%22operator%22%3A%22is%22%2C%22value%22%3A{value}%7D%5D%7D%2C%22type%22%3A%22movies%22%7D"
17
17
 
18
18
 
19
+ ENCODER = ("plugin://script.skinvariables/?info=get_encoded_string&unencoded_paths=true"
20
+ "&&$INFO[ListItem.Label]&&$INFO[ListItem.Title]")
21
+
22
+
19
23
  def path(value, field="title"):
20
24
  """An xsp path with a single "is" rule, percent-encoded as skins write it."""
21
25
  return PATH.format(field=field, value=value)
22
26
 
23
27
 
28
+ def url_rows(home, includes="<includes/>"):
29
+ """The xsp and plugin URL rows a one-window skin with that Home.xml and Includes.xml gives."""
30
+ files = {
31
+ "addon.xml": '<addon id="skin.t" version="1.0.0"><requires><import addon="xbmc.gui" '
32
+ 'version="5.17.0"/></requires><extension point="xbmc.gui.skin">'
33
+ '<res folder="16x9" default="true"/></extension></addon>',
34
+ "16x9/Includes.xml": includes,
35
+ "16x9/Home.xml": home,
36
+ }
37
+ with tempfile.TemporaryDirectory() as skin:
38
+ for rel, text in files.items():
39
+ os.makedirs(os.path.dirname(os.path.join(skin, rel)), exist_ok=True)
40
+ with open(os.path.join(skin, rel), "w", encoding="utf-8") as f:
41
+ f.write(text)
42
+ provider = InfoProvider()
43
+ provider.settings = {}
44
+ provider.init_addon(skin)
45
+ return [r for r in provider.check_values() if r.get("rule", "").startswith(("xml.xsp", "xml.plugin"))]
46
+
47
+
24
48
  class TestUrlValues(unittest.TestCase):
25
49
  """Sorting of each runtime value in a rule's value list into raw, untraced or safe."""
26
50
 
@@ -78,7 +102,17 @@ class TestUrlValues(unittest.TestCase):
78
102
  f.write('<window><controls><control type="list" id="9110"><content>'
79
103
  '<item><label>a</label><property name="Dir">$INFO[ListItem.Director]</property></item>'
80
104
  '</content></control></controls></window>')
81
- self.assertEqual(value_sources([path]), {"item:9110:dir": ["$INFO[ListItem.Director]"]})
105
+ self.assertEqual(value_sources([path]), {"item:9110:dir": ["$INFO[ListItem.Director]"], "content:9110": [""]})
106
+
107
+ def test_scan_reads_each_list_path_by_id(self):
108
+ with tempfile.TemporaryDirectory() as folder:
109
+ path = os.path.join(folder, "Home.xml")
110
+ with open(path, "w", encoding="utf-8") as f:
111
+ f.write('<window><controls><control type="list" id="4001"><content>'
112
+ f'{ENCODER.replace("&", "&amp;")}</content></control>'
113
+ '<control type="list"><content>videodb://movies/titles/</content></control>'
114
+ '</controls></window>')
115
+ self.assertEqual(value_sources([path]), {"content:4001": [ENCODER]})
82
116
 
83
117
  def test_keyboard_skin_string_is_raw(self):
84
118
  self.assertEqual(self.kinds("%5B%22$INFO[Skin.String(search)]%22%5D", {"skin:search": ["keyboard"]}),
@@ -102,7 +136,7 @@ class TestUrlRows(unittest.TestCase):
102
136
  """Xsp and plugin URL rows: errors for raw or escaped typed values, warnings for the rest."""
103
137
 
104
138
  def test_rows(self):
105
- """Check the rule, severity and subject of every URL fault in one window."""
139
+ """Test the rule, severity and subject of every URL fault in one window."""
106
140
  home = (
107
141
  '<window><controls>'
108
142
  '<control type="button" id="10">'
@@ -125,22 +159,7 @@ class TestUrlRows(unittest.TestCase):
125
159
  '<control type="list" id="56"><content>' + path("%5B%22$INFO[Control.GetLabel(9).Index(1)]%22%5D").replace("&", "&amp;")
126
160
  + '</content></control>'
127
161
  '</controls></window>')
128
- files = {
129
- "addon.xml": '<addon id="skin.t" version="1.0.0"><requires><import addon="xbmc.gui" '
130
- 'version="5.17.0"/></requires><extension point="xbmc.gui.skin">'
131
- '<res folder="16x9" default="true"/></extension></addon>',
132
- "16x9/Includes.xml": "<includes/>",
133
- "16x9/Home.xml": home,
134
- }
135
- with tempfile.TemporaryDirectory() as skin:
136
- for rel, text in files.items():
137
- os.makedirs(os.path.dirname(os.path.join(skin, rel)), exist_ok=True)
138
- with open(os.path.join(skin, rel), "w", encoding="utf-8") as f:
139
- f.write(text)
140
- provider = InfoProvider()
141
- provider.settings = {}
142
- provider.init_addon(skin)
143
- rows = [r for r in provider.check_values() if r.get("rule", "").startswith(("xml.xsp", "xml.plugin"))]
162
+ rows = url_rows(home)
144
163
  found = sorted((r["rule"], r["severity"], r["subject"]) for r in rows)
145
164
  self.assertEqual(found, [
146
165
  ("xml.plugin-typed-text", "warning", "$INFO[Control.GetLabel(312).Index(1)]"),
@@ -156,6 +175,29 @@ class TestUrlRows(unittest.TestCase):
156
175
  self.assertIn("'Law & Order' empties the list", error)
157
176
  self.assertIn("URL-encode it first", error)
158
177
 
178
+ def test_encoder_list_labels_in_a_plugin_url(self):
179
+ """The labels of an encoder list pass through variables unreported."""
180
+ includes = (
181
+ '<includes>'
182
+ '<variable name="Path_Encoded_Label"><value>$INFO[Container(4001).ListItemAbsolute(0).Label]</value></variable>'
183
+ '<variable name="Path_Raw_Label"><value>$INFO[Container(4002).ListItemAbsolute(0).Label]</value></variable>'
184
+ '<variable name="Path_Youtube_Query"><value>$VAR[Path_Encoded_Label]</value></variable>'
185
+ '</includes>')
186
+ home = (
187
+ '<window><controls>'
188
+ '<control type="list" id="4001"><content>' + ENCODER.replace("&", "&amp;") + '</content></control>'
189
+ '<control type="list" id="4002"><content>videodb://tvshows/titles/</content></control>'
190
+ '<control type="button" id="10">'
191
+ '<onclick>ActivateWindow(Videos,plugin://plugin.video.youtube/kodion/search/query/'
192
+ '?q=$VAR[Path_Youtube_Query],return)</onclick>'
193
+ '<onclick>ActivateWindow(Videos,plugin://plugin.video.youtube/kodion/search/query/'
194
+ '?q=$VAR[Path_Raw_Label],return)</onclick>'
195
+ '</control>'
196
+ '</controls></window>')
197
+ rows = url_rows(home, includes)
198
+ self.assertEqual(sorted((r["rule"], r["subject"]) for r in rows),
199
+ [("xml.plugin-unencoded", "$INFO[Container(4002).ListItemAbsolute(0).Label]")])
200
+
159
201
 
160
202
  class TestPluginValues(unittest.TestCase):
161
203
  """Runtime values in a plugin URL's query, which the add-on splits on & and decodes."""
@@ -180,6 +222,10 @@ class TestPluginValues(unittest.TestCase):
180
222
  text = 'RunPlugin("plugin://script.skinvariables/?info=get_encoded_string&window_prop=E&&$INFO[ListItem.Label]")'
181
223
  self.assertEqual(self.kinds(text), [])
182
224
 
225
+ def test_a_space_stays_inside_the_query(self):
226
+ self.assertEqual(self.kinds("plugin://p/?q=$INFO[ListItem.Artist] $INFO[ListItem.Album] Album"),
227
+ [("$INFO[ListItem.Artist]", RAW, None), ("$INFO[ListItem.Album]", RAW, None)])
228
+
183
229
  def test_only_the_query_counts(self):
184
230
  self.assertEqual(self.kinds("plugin://x/$INFO[ListItem.Title]/"), [])
185
231
  self.assertEqual(self.kinds("RunPlugin(plugin://x/?a=b) + $INFO[ListItem.Title]"), [])
@@ -199,6 +245,26 @@ class TestPluginValues(unittest.TestCase):
199
245
  self.assertEqual(self.kinds("plugin://p/?genre=$VAR[Genre]", variables=variables),
200
246
  [("$VAR[Genre]", RAW, None)])
201
247
 
248
+ def test_labels_of_an_encoder_list_are_safe(self):
249
+ """An encoder list holds an encoded label per item."""
250
+ sources = {"content:4001": [ENCODER]}
251
+ variables = {"Path_Encoded_Label": ["$INFO[Container(4001).ListItemAbsolute(0).Label]"],
252
+ "Path_Youtube_Query": ["$VAR[Path_Encoded_Label]"], "Encoder": [ENCODER]}
253
+ self.assertEqual(self.kinds("plugin://p/?q=$VAR[Path_Youtube_Query]", sources, variables), [])
254
+ self.assertEqual(self.kinds("plugin://p/?q=$INFO[Container(4001).ListItem(1).Label]", sources), [])
255
+ self.assertEqual(self.kinds("plugin://p/?q=$INFO[Container(4001).ListItem.Label]",
256
+ {"content:4001": ["$VAR[Encoder]"]}, variables), [])
257
+
258
+ def test_labels_of_other_lists_stay_raw(self):
259
+ label = "$INFO[Container(4001).ListItemAbsolute(0).Label]"
260
+ text = f"plugin://p/?q={label}"
261
+ self.assertEqual(self.kinds(text), [(label, RAW, None)])
262
+ self.assertEqual(self.kinds(text, {"content:4001": ["videodb://movies/titles/"]}), [(label, RAW, None)])
263
+ self.assertEqual(self.kinds(text, {"content:4001": [ENCODER, ""]}), [(label, RAW, None)])
264
+ self.assertEqual(self.kinds(text, {"content:4001": ["$VAR[Missing]"]}), [(label, RAW, None)])
265
+ self.assertEqual(self.kinds("plugin://p/?q=$INFO[Container(4001).ListItem.Title]", {"content:4001": [ENCODER]}),
266
+ [("$INFO[Container(4001).ListItem.Title]", RAW, None)])
267
+
202
268
  def test_time_date_and_sort_words_need_no_encoding(self):
203
269
  self.assertEqual(self.kinds("plugin://p/?t=$INFO[System.Time(hh)]&d=$INFO[System.Date]"
204
270
  "&s=$INFO[Container.SortMethod]&o=$INFO[Container.SortOrder]"), [])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes