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