visit: function vmh_visit(chan) {
		if (log.enabled) {
			let msg = chan.URI.spec + "\nRequest:\n";
			let visitor = {
				visitHeader: function(header, value) {
					msg += header + ": " + value + "\n";
				}
			};
			chan.visitRequestHeaders(visitor);
			msg += "\nResponse:\n";
			chan.visitResponseHeaders(visitor);
			log(LOG_DEBUG, msg);
		}
		try {
			this.type = chan.getResponseHeader("content-type");
			var ch = this.type.match(/charset=['"]?([\w\d_-]+)/i);
			if (ch && ch[1].length) {
				log(LOG_DEBUG, "visitHeader: found override to " + ch[1]);
				this._charset = this.overrideCharset = identity(ch[1]);
			}
		}
		catch (ex) {}

		try {
			this.encoding = identity(chan.getResponseHeader("content-encoding"));
		}
		catch (ex) {}

		try {
			this.acceptRanges = !/none/i.test(chan.getResponseHeader("accept-ranges"));
			if (!this.acceptRanges) {
				this.acceptRanges = !~this.acceptRanges.toLowerCase().indexOf('none');
			}
		}
		catch (ex) {}

		let contentLength;
		try {
			contentLength = parseInt(chan.getResponseHeader("content-length"), 10);
		}
		catch (ex) {}
		if (contentLength < 0 || isNaN(contentLength)) {
			try {
				contentLength = parseInt(chan.getResponseHeader("content-range").split("/").pop(), 10);
			} catch (ex) {}
		}
		if (contentLength > 0 && !isNaN(contentLength)) {
			this.contentLength = contentLength;
		}

		try {
			let digest = chan.getResponseHeader("digest").replace(/,/g, ";");
			digest = ";" + digest;
			for (let t in DTA.SUPPORTED_HASHES_ALIASES) {
				try {
					let v = Services.mimeheader.getParameter(digest, t, this._charset, true, {});
					if (!v) {
						continue;
					}
					v = atob(v);
					v = new DTA.Hash(v, t);
					if (!this.hash || this.hash.q < v.q) {
						this.hash = v;
					}
				}
				catch (ex) {
					// no-op
				}
			}
		}
		catch (ex) {}
		try {
			let poweredby = chan.getResponseHeader("x-powered-by");
			if (!!poweredby) {
				this.relaxSize = true;
			}
		}
		catch (ex) {}

		try {
			delete this.mirrors;
			let links = chan.getResponseHeader("Link").split(/,\s*/g);
			for (let link of links) {
				try {
					let linkURI = Services.mimeheader.getParameter(link, null, null, true, {})
						.replace(/[<>]/g, '');
					const rel = Services.mimeheader.getParameter(link, "rel", null, true, {});
					if (rel === "describedby") {
						const type = Services.mimeheader.getParameter(link, "type", null, true, {});
						if (type === "application/metalink4+xml") {
							this.metaDescribedBy = Services.io.newURI(linkURI, null, null);
						}
					}
					else if (rel === "duplicate") {
						linkURI = Services.io.newURI(linkURI, null, null);
						let pri, pref, depth;
						try {
							pri = Services.mimeheader.getParameter(link, "pri", null, true, {});
							pri = parseInt(pri, 10);
							try {
								pref = Services.mimeheader.getParameter(link, "pref", null, true, {});
								pri = 1;
							}
							catch (ex) {}
							try{
								depth = Services.mimeheader.getParameter(link, "depth", null, true, {});
							}
							catch (ex) {}
							try {
								const geo = Services.mimeheader.getParameter(link, "geo", null, true, {})
									.slice(0,2).toLowerCase();
								if (~LOCALE.indexOf(geo)) {
									pri = Math.max(pri / 4, 1);
								}
							}
							catch (ex) {}
						}
						catch (ex) {}
						if (!this.mirrors) {
							this.mirrors = [];
						}
						this.mirrors.push(new DTA.URL(linkURI, pri));
					}
				}
				catch (ex) {
					log(LOG_ERROR, "VM: failed to process a link", ex);
				}
			}
			if (this.mirrors) {
				normalizeMetaPrefs(this.mirrors);
			}
		}
		catch (ex) {
			log(LOG_DEBUG, "VM: failed to process links", ex);
		}

		for (let header in this.cmpKeys) {
			try {
				let value = chan.getResponseHeader(header);
				this[header] = value;
			}
			catch (ex) {}
		}

		if ("etag" in this) {
			let etag = this.etag;
			this.etag = etag
				.replace(/^(?:[Ww]\/)?"(.+)"$/, '$1')
				.replace(/^[a-f\d]+-([a-f\d]+)-([a-f\d]+)$/, '$1-$2')
				.replace(/^([a-f\d]+):[a-f\d]{1,6}$/, '$1');
			log(LOG_DEBUG, "Etag: " + this.etag + " - " + etag);
		}
		if ("last-modified" in this) {
			try {
				this.time = getTimestamp(this["last-modified"]);
			}
			catch (ex) {}
		}

		try {
			this._checkFileName(chan.getResponseHeader("content-disposition"));
		}
		catch (ex) {}
		if (!("fileName" in this) && ("type" in this)) {
			this._checkFileName(this.type);
		}
	},
	parse(aReferrer) {
		if (aReferrer && 'spec' in aReferrer) {
			aReferrer = aReferrer.spec;
		}

		let doc = this._doc;
		let root = doc.documentElement;
		let downloads = [];

		let files = this.getNodes(doc, '/ml:metalink/ml:file');
		if (!files.length) {
			throw new Exception("No valid file nodes");
		}
		for (let file of files) {
			let fileName = file.getAttribute('name');
			if (!fileName) {
				throw new Exception("LocalFile name not provided!");
			}
			let referrer = null;
			if (file.hasAttributeNS(NS_DTA, 'referrer')) {
				referrer = file.getAttributeNS(NS_DTA, 'referrer');
			}
			else {
				referrer = aReferrer;
			}
			let num = null;
			if (file.hasAttributeNS(NS_DTA, 'num')) {
				try {
					num = parseInt(file.getAttributeNS(NS_DTA, 'num'), 10);
				}
				catch (ex) {
					/* no-op */
				}
			}
			if (!num) {
				num = DTA.currentSeries();
			}
			let startDate = new Date();
			if (file.hasAttributeNS(NS_DTA, 'startDate')) {
				try {
					startDate = new Date(parseInt(file.getAttributeNS(NS_DTA, 'startDate'), 10));
				}
				catch (ex) {
					/* no-op */
				}
			}

			let urls = [];
			let urlNodes = this.getNodes(file, 'ml:url');
			for (var url of urlNodes) {
				let preference = 1;
				let charset = doc.characterSet;
				if (url.hasAttributeNS(NS_DTA, 'charset')) {
					charset = url.getAttributeNS(NS_DTA, 'charset');
				}

				let uri = null;
				try {
					uri = this.checkURL(url.textContent.trim());
					if (!uri) {
						throw new Exception("Invalid url");
					}
					uri = Services.io.newURI(uri, charset, null);
				}
				catch (ex) {
					log(LOG_ERROR, "Failed to parse URL" + url.textContent, ex);
					continue;
				}

				if (url.hasAttribute('priority')) {
					let a = parseInt(url.getAttribute('priority'), 10);
					if (a > 0) {
						preference = a;
					}
				}
				if (url.hasAttribute('location')) {
					let a = url.getAttribute('location').slice(0,2).toLowerCase();
					if (~LOCALE.indexOf(a)) {
						preference = Math.max(preference / 4, 1);
					}
				}
				urls.push(new DTA.URL(uri, preference));
			}
			if (!urls.length) {
				continue;
			}
			normalizeMetaPrefs(urls);

			let size = this.getSingle(file, 'size');
			size = parseInt(size, 10);
			if (!isFinite(size)) {
				size = 0;
			}

			let hash = null;
			for (let h of this.getNodes(file, 'ml:hash')) {
				try {
					h = new DTA.Hash(h.textContent.trim(), h.getAttribute('type'));
					if (!hash || hash.q < h.q) {
						hash = h;
					}
				}
				catch (ex) {
					log(LOG_ERROR, "Failed to parse hash: " + h.textContent.trim() + "/" + h.getAttribute('type'), ex);
				}
			}
			if (hash) {
				Cu.reportError(hash);
				hash = new DTA.HashCollection(hash);
				let pieces = this.getNodes(file, 'ml:pieces');
				if (pieces.length) {
					pieces = pieces[0];
					let type = pieces.getAttribute('type').trim();
					try {
						hash.parLength = parseInt(pieces.getAttribute('length'), 10);
						if (!isFinite(hash.parLength) || hash.parLength < 1) {
							throw new Exception("Invalid pieces length");
						}
						for (let piece of this.getNodes(pieces, 'ml:hash')) {
							try {
								hash.add(new DTA.Hash(piece.textContent.trim(), type));
							}
							catch (ex) {
								log(LOG_ERROR, "Failed to parse piece", ex);
								throw ex;
							}
						}
						if (size && hash.parLength * hash.partials.length < size) {
							throw new Exception("too few partials");
						}
						else if(size && (hash.partials.length - 1) * hash.parLength > size) {
							throw new Exception("too many partials");
						}
						log(LOG_DEBUG, "loaded " + hash.partials.length + " partials");
					}
					catch (ex) {
						log(LOG_ERROR, "Failed to parse pieces", ex);
						hash = new DTA.HashCollection(hash.full);
					}
				}
			}

			let desc = this.getSingle(file, 'description');
			if (!desc) {
				desc = this.getSingle(root, 'description');
			}
			downloads.push({
				'url': new UrlManager(urls),
				'fileName': fileName,
				'referrer': referrer ? referrer : null,
				'numIstance': num,
				'title': '',
				'description': desc,
				'startDate': startDate,
				'hashCollection': hash,
				'license': this.getLinkRes(file, "license"),
				'publisher': this.getLinkRes(file, "publisher"),
				'identity': this.getSingle(file, "identity"),
				'copyright': this.getSingle(file, "copyright"),
				'size': size,
				'version': this.getSingle(file, "version"),
				'logo': this.checkURL(this.getSingle(file, "logo", ['data'])),
				'lang': this.getSingle(file, "language"),
				'sys': this.getSingle(file, "os"),
				'mirrors': urls.length,
				'selected': true,
				'fromMetalink': true
			});
		}

		if (!downloads.length) {
			throw new Exception("No valid files to process");
		}

		let info = {
			'identity': this.getSingle(root, "identity"),
			'description': this.getSingle(root, "description"),
			'logo': this.checkURL(this.getSingle(root, "logo", ['data'])),
			'license': this.getLinkRes(root, "license"),
			'publisher': this.getLinkRes(root, "publisher"),
			'start': false
		};
		return new Metalink(downloads, info, "Metalinker Version 4.0 (RFC5854/IETF)");
	}