]> git.phdru.name Git - extfs.d.git/blob - xml
Feat: Update for Python 3
[extfs.d.git] / xml
1 #! /usr/bin/env python3
2 """XML Virtual FileSystem for Midnight Commander
3
4 The script requires Midnight Commander 3.1+
5 (http://www.midnight-commander.org/), Python 2.4+ (http://www.python.org/).
6
7 For mc 4.7+ just put the script in $HOME/[.local/share/].mc/extfs.d.
8 For older versions put it in /usr/[local/][lib|share]/mc/extfs
9 and add a line "xml" to the /usr/[local/][lib|share]/mc/extfs/extfs.ini.
10 Make the script executable.
11
12 For mc 4.7+ run this "cd" command in the Midnight Commander (in the "bindings"
13 file the command is "%cd"): cd file/xml://; in older versions it is
14 cd file#xml, where "file" is the name of your XML file.
15
16 See detailed installation instructions at
17 https://phdru.name/Software/mc/xml_INSTALL.html.
18
19 The VFS represents tags as directories; the directories are numbered to
20 distinguish tags with the same name; numbering also helps to sort tags by their
21 order in XML instead of sorting them by name and prevents name clash when tag
22 names coincide with the names of special files used by XML VFS. Attributes,
23 text nodes and comments are represented as text files; attributes are shown in
24 a file named "attributes", attributes are listed in the file as name=value
25 lines (I deliberately ignore a small chance of newline characters in values);
26 names and values are reencoded to the console encoding. Text nodes and comments
27 are collected in a file named "text", stripped and reencoded. The filesystem is
28 read-only.
29
30 Date/time for all directories/files set to the last modification time of the
31 XML file.
32
33 Implementation based on minidom doesn't understand namespaces, it just shows
34 them among other attributes. ElementTree-based implementation doesn't show
35 namespaces at all. Implementation based on lxml.etree shows namespaces in a
36 separate file "namespaces".
37
38 It is useful to have a top-down view on an XML structure but it's especially
39 convenient to extract text values from tags. One can get, for example, a
40 base64-encoded image - just walk down the VFS to the tag's directory and copy
41 its text file to a real file.
42
43 The VFS was inspired by a FUSE xmlfs: https://github.com/halhen/xmlfs
44
45 """
46
47 __version__ = "1.2.0"
48 __author__ = "Oleg Broytman <phd@phdru.name>"
49 __copyright__ = "Copyright (C) 2013-2023 PhiloSoft Design"
50 __license__ = "GPL"
51
52 # Can be None for default choice, 'lxml', 'elementtree' or 'minidom'.
53 force_implementation = None
54
55 use_minidom = True
56 use_elementtree = False
57 use_lxml = False
58
59 import math
60 from os.path import getmtime
61 import sys
62 from time import localtime
63 import xml.dom.minidom
64
65 if sys.version_info[0] == 2:
66     try:
67         import xml.etree.ElementTree as ET
68     except ImportError:
69         pass
70     else:
71         use_elementtree = True
72
73 try:
74     import lxml.etree as etree
75 except ImportError:
76     pass
77 else:
78     use_lxml = True
79
80 try:
81     import locale
82     use_locale = True
83 except ImportError:
84     use_locale = False
85
86 if use_locale:
87     # Get the default charset.
88     try:
89         lcAll = locale.getdefaultlocale()
90     except locale.Error as err:
91         print("WARNING:", err, file=sys.stderr)
92         lcAll = []
93
94     if len(lcAll) == 2:
95         default_encoding = lcAll[1]
96     else:
97         try:
98             default_encoding = locale.getpreferredencoding()
99         except locale.Error as err:
100             print("WARNING:", err, file=sys.stderr)
101             default_encoding = sys.getdefaultencoding()
102 else:
103     default_encoding = sys.getdefaultencoding()
104
105 import logging
106 logger = logging.getLogger('xml-mcextfs')
107 log_err_handler = logging.StreamHandler(sys.stderr)
108 logger.addHandler(log_err_handler)
109 logger.setLevel(logging.INFO)
110
111 if len(sys.argv) < 3:
112     logger.critical("""\
113 XML Virtual FileSystem for Midnight Commander version %s
114 Author: %s
115 %s
116
117 This is not a program. Put the script in $HOME/[.local/share/].mc/extfs.d or
118 /usr/[local/][lib|share]/mc/extfs. For more information read the source!""",
119                     __version__, __author__, __copyright__)
120     sys.exit(1)
121
122
123 locale.setlocale(locale.LC_ALL, '')
124
125
126 class XmlVfs(object):
127     """Abstract base class"""
128
129     supports_namespaces = False
130
131     def __init__(self):
132         self.xml_file = sys.argv[2]
133         self.parse()
134
135     def list(self):
136         Y, m, d, H, M = localtime(getmtime(self.xml_file))[0:5]
137         self.xml_file_dt = "%02d-%02d-%d %02d:%02d" % (m, d, Y, H, M)
138
139         root_comments = self.get_root_comments()
140         if root_comments:
141             print("-r--r--r-- 1 user group %d %s text" % (
142                 len(root_comments), self.xml_file_dt))
143
144         self._list(self.getroot())
145
146     def _list(self, node, path=''):
147         n = len(self.getchildren(node))
148         if n:
149             width = int(math.log10(n)) + 1
150             template = "%%0%dd" % width
151         else:
152             template = "%d"
153         n = 0
154         for element in self.getchildren(node):
155             if not self.istag(element):
156                 continue
157             n += 1
158             tag = self.getlocalname(self.gettag(element))
159             if path:
160                 subpath = '%s/%s %s' % (path, template % n, tag)
161             else:
162                 subpath = '%s %s' % (template % n, tag)
163             subpath_encoded = subpath.encode(default_encoding, "replace")
164             print("dr-xr-xr-x 1 user group 0 %s %s" % (
165                 self.xml_file_dt, subpath_encoded))
166             if self.getattrs(element):
167                 attr_text = self.attrs2text(element)
168                 print("-r--r--r-- 1 user group %d %s %s/attributes" % (
169                     len(attr_text), self.xml_file_dt, subpath_encoded))
170             if self.supports_namespaces and self.has_ns(element):
171                 ns_text = self.ns2text(element)
172                 print("-r--r--r-- 1 user group %d %s %s/namespaces" % (
173                     len(ns_text), self.xml_file_dt, subpath_encoded))
174             text = self.collect_text(element)
175             if text:
176                 print("-r--r--r-- 1 user group %d %s %s/text" % (
177                     len(text), self.xml_file_dt, subpath_encoded))
178             self._list(element, subpath)
179
180     def get_tag_node(self, node, i):
181         n = 0
182         for element in self.getchildren(node):
183             if self.istag(element):
184                 n += 1
185                 if n == i:
186                     return element
187         xml_error('There are less than %d nodes' % i)
188
189     def attrs2text(self, node):
190         attr_accumulator = []
191         for name, value in self.getattrs(node):
192             name = self.getlocalname(name).encode(default_encoding, "replace")
193             value = value.encode(default_encoding, "replace")
194             attr_accumulator.append("%s=%s" % (name, value))
195         return '\n'.join(attr_accumulator)
196
197     def has_ns(self, node):
198         return False
199
200
201 class MiniDOMXmlVfs(XmlVfs):
202     def parse(self):
203         self.document = xml.dom.minidom.parse(self.xml_file)
204
205     def getattrs(self, node):
206         attrs = node.attributes
207         attrs = [attrs.item(i) for i in range(attrs.length)]
208         return [(a.name, a.value) for a in attrs]
209
210     def collect_text(self, node):
211         text_accumulator = []
212         for element in node.childNodes:
213             if element.localName:
214                 continue
215             elif element.nodeType == element.COMMENT_NODE:
216                 text = u"<!--%s-->" % element.nodeValue
217             elif element.nodeType == element.TEXT_NODE:
218                 text = element.nodeValue.strip()
219             else:
220                 xml_error("Unknown node type %d" % element.nodeType)
221             if text:
222                 text_accumulator.append(text)
223         return '\n'.join(text_accumulator).encode(default_encoding, "replace")
224
225     def getroot(self):
226         return self.document
227
228     def get_root_comments(self):
229         return self.collect_text(self.document)
230
231     def getchildren(self, node):
232         return node.childNodes
233
234     def gettag(self, node):
235         return node.localName
236
237     def istag(self, node):
238         return bool(node.localName)
239
240     def getlocalname(self, name):
241         return name
242
243
244 if use_elementtree or use_lxml:
245     class CommonEtreeXmlVfs(XmlVfs):
246         def getattrs(self, node):
247             return node.attrib.items()
248
249         def collect_text(self, node):
250             text_accumulator = []
251             if node.text:
252                 text = node.text.strip()
253                 if text:
254                     text_accumulator.append(text)
255             for element in node:
256                 if not self.istag(element):
257                     text = u"<!--%s-->" % element.text
258                     text_accumulator.append(text)
259             if node.tail:
260                 text = node.tail.strip()
261                 if text:
262                     text_accumulator.append(text)
263             return '\n'.join(text_accumulator).encode(
264                 default_encoding, "replace")
265
266         def getchildren(self, node):
267             return list(node)
268
269         def gettag(self, node):
270             return node.tag
271
272         def istag(self, node):
273             return isinstance(node.tag, basestring)
274
275
276 if use_elementtree:
277     class ElementTreeXmlVfs(CommonEtreeXmlVfs):
278         def parse(self):
279             # Copied from http://effbot.org/zone/element-pi.htm
280
281             class PIParser(ET.XMLTreeBuilder):
282
283                 def __init__(self):
284                     ET.XMLTreeBuilder.__init__(self)
285                     # assumes ElementTree 1.2.X
286                     self._parser.CommentHandler = self.handle_comment
287                     self._parser.ProcessingInstructionHandler = self.handle_pi
288                     self._target.start("document", {})
289
290                 def close(self):
291                     self._target.end("document")
292                     return ET.XMLTreeBuilder.close(self)
293
294                 def handle_comment(self, data):
295                     self._target.start(ET.Comment, {})
296                     self._target.data(data)
297                     self._target.end(ET.Comment)
298
299                 def handle_pi(self, target, data):
300                     self._target.start(ET.PI, {})
301                     self._target.data(target + " " + data)
302                     self._target.end(ET.PI)
303
304             self.document = ET.parse(self.xml_file, PIParser())
305
306         def getroot(self):
307             return self.document.getroot()
308
309         def get_root_comments(self):
310             text_accumulator = []
311             for element in self.getroot():
312                 if not self.istag(element):
313                     text = u"<!--%s-->" % element.text
314                     text_accumulator.append(text)
315             return '\n'.join(text_accumulator).encode(
316                 default_encoding, "replace")
317
318         def getlocalname(self, name):
319             if name.startswith('{'):
320                 name = name.split('}', 1)[1]  # Remove XML namespace
321             return name
322
323
324 if use_lxml:
325     class LxmlEtreeXmlVfs(CommonEtreeXmlVfs):
326         supports_namespaces = True
327
328         def parse(self):
329             self.document = etree.parse(self.xml_file)
330
331         def getroot(self):
332             return [self.document.getroot()]
333
334         def get_root_comments(self):
335             text_accumulator = []
336             for element in self.document.getroot().itersiblings(
337                     tag=etree.Comment, preceding=True):
338                 text = u"<!--%s-->" % element.text
339                 text_accumulator.append(text)
340             return '\n'.join(text_accumulator).encode(
341                 default_encoding, "replace")
342
343         def getlocalname(self, name):
344             return etree.QName(name).localname
345
346         def _get_local_ns(self, node):
347             this_nsmap = node.nsmap
348             parent = node.getparent()
349             if parent is not None:
350                 parent_nsmap = parent.nsmap
351                 for key in parent_nsmap:
352                     if this_nsmap[key] == parent_nsmap[key]:
353                         del this_nsmap[key]
354             return this_nsmap
355
356         def has_ns(self, node):
357             return bool(self._get_local_ns(node))
358
359         def ns2text(self, node):
360             ns_accumulator = []
361             for name, value in self._get_local_ns(node).items():
362                 if name:
363                     name = name.encode(default_encoding, "replace")
364                 else:
365                     name = 'xmlns'
366                 value = value.encode(default_encoding, "replace")
367                 ns_accumulator.append("%s=%s" % (name, value))
368             return '\n'.join(ns_accumulator)
369
370
371 def build_xmlvfs():
372     if force_implementation is None:
373         if use_lxml:
374             return LxmlEtreeXmlVfs()
375         elif use_elementtree:
376             return ElementTreeXmlVfs()
377         else:
378             return MiniDOMXmlVfs()
379     elif force_implementation == 'minidom':
380         return MiniDOMXmlVfs()
381     elif force_implementation == 'elementtree':
382         return ElementTreeXmlVfs()
383     elif force_implementation == 'lxml':
384         return LxmlEtreeXmlVfs()
385     else:
386         raise ValueError('Unknown implementation "%s", expected "minidom", '
387                          '"elementtree" or "lxml"' % force_implementation)
388
389
390 def mcxml_list():
391     """List the entire VFS"""
392
393     xmlvfs = build_xmlvfs()
394     xmlvfs.list()
395
396
397 def mcxml_copyout():
398     """Extract a file from the VFS"""
399
400     xmlvfs = build_xmlvfs()
401     xml_filename = sys.argv[3]
402     real_filename = sys.argv[4]
403
404     node = xmlvfs.getroot()
405     for path_comp in xml_filename.split('/'):
406         if ' ' in path_comp:
407             i = int(path_comp.split(' ', 1)[0])
408             node = xmlvfs.get_tag_node(node, i)
409         elif path_comp in ('attributes', 'namespaces', 'text'):
410             break
411         else:
412             xml_error('Unknown file')
413
414     if path_comp == 'attributes':
415         if xmlvfs.getattrs(node):
416             text = xmlvfs.attrs2text(node)
417         else:
418             xml_error('There are no attributes')
419
420     elif path_comp == 'namespaces':
421         if xmlvfs.supports_namespaces and xmlvfs.has_ns(node):
422             text = xmlvfs.ns2text(node)
423         else:
424             xml_error('There are no namespaces')
425
426     elif path_comp == 'text':
427         if '/' in xml_filename:
428             text = xmlvfs.collect_text(node)
429         else:
430             text = xmlvfs.get_root_comments()
431
432     else:
433         xml_error('Unknown file')
434
435     outfile = open(real_filename, 'w')
436     outfile.write(text)
437     outfile.close()
438
439
440 def mcxml_copyin():
441     """Put a file to the VFS"""
442     sys.exit("XML VFS doesn't support adding files (read-only filesystem)")
443
444
445 def mcxml_rm():
446     """Remove a file from the VFS"""
447     sys.exit("XML VFS doesn't support removing files/directories "
448              "(read-only filesystem)")
449
450 mcxml_rmdir = mcxml_rm
451
452
453 def mcxml_mkdir():
454     """Create a directory in the VFS"""
455     sys.exit("XML VFS doesn't support creating directories "
456              "(read-only filesystem)")
457
458
459 def xml_error(error_str):
460     logger.critical("Error walking XML file: %s", error_str)
461     sys.exit(1)
462
463 command = sys.argv[1]
464 procname = "mcxml_" + command
465
466 g = globals()
467 if procname not in g:
468     logger.critical("Unknown command %s", command)
469     sys.exit(1)
470
471 try:
472     g[procname]()
473 except SystemExit:
474     raise
475 except Exception:
476     logger.exception("Error during run")