text: /*! magnet-uri. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
  module.exports = magnetURIDecode
  module.exports.decode = magnetURIDecode
  module.exports.encode = magnetURIEncode
  
  const base32 = require('thirty-two')
  const bep53Range = require('bep53-range')
  
  /**
   * Parse a magnet URI and return an object of keys/values
   *
   * @param  {string} uri
   * @return {Object} parsed uri
   */
  function magnetURIDecode (uri) {
    const result = {}
  
    // Support 'magnet:' and 'stream-magnet:' uris
    const data = uri.split('magnet:?')[1]
  
    const params = (data && data.length >= 0)
      ? data.split('&')
      : []
  
    params.forEach(param => {
      const keyval = param.split('=')
  
      // This keyval is invalid, skip it
      if (keyval.length !== 2) return
  
      const key = keyval[0]
      let val = keyval[1]
  
      // Clean up torrent name
      if (key === 'dn') val = decodeURIComponent(val).replace(/\+/g, ' ')
  
      // Address tracker (tr), exact source (xs), and acceptable source (as) are encoded
      // URIs, so decode them
      if (key === 'tr' || key === 'xs' || key === 'as' || key === 'ws') {
        val = decodeURIComponent(val)
      }
  
      // Return keywords as an array
      if (key === 'kt') val = decodeURIComponent(val).split('+')
  
      // Cast file index (ix) to a number
      if (key === 'ix') val = Number(val)
  
      // bep53
      if (key === 'so') val = bep53Range.parse(decodeURIComponent(val).split(','))
  
      // If there are repeated parameters, return an array of values
      if (result[key]) {
        if (!Array.isArray(result[key])) {
          result[key] = [result[key]]
        }
  
        result[key].push(val)
      } else {
        result[key] = val
      }
    })
  
    // Convenience properties for parity with `parse-torrent-file` module
    let m
    if (result.xt) {
      const xts = Array.isArray(result.xt) ? result.xt : [result.xt]
      xts.forEach(xt => {
        if ((m = xt.match(/^urn:btih:(.{40})/))) {
          result.infoHash = m[1].toLowerCase()
        } else if ((m = xt.match(/^urn:btih:(.{32})/))) {
          const decodedStr = base32.decode(m[1])
          result.infoHash = Buffer.from(decodedStr, 'binary').toString('hex')
        }
      })
    }
  
    if (result.xs) {
      const xss = Array.isArray(result.xs) ? result.xs : [result.xs]
      xss.forEach(xs => {
        if ((m = xs.match(/^urn:btpk:(.{64})/))) {
          result.publicKey = m[1].toLowerCase()
        }
      })
    }
  
    if (result.infoHash) result.infoHashBuffer = Buffer.from(result.infoHash, 'hex')
    if (result.publicKey) result.publicKeyBuffer = Buffer.from(result.publicKey, 'hex')
  
    if (result.dn) result.name = result.dn
    if (result.kt) result.keywords = result.kt
  
    result.announce = []
    if (typeof result.tr === 'string' || Array.isArray(result.tr)) {
      result.announce = result.announce.concat(result.tr)
    }
  
    result.urlList = []
    if (typeof result.as === 'string' || Array.isArray(result.as)) {
      result.urlList = result.urlList.concat(result.as)
    }
    if (typeof result.ws === 'string' || Array.isArray(result.ws)) {
      result.urlList = result.urlList.concat(result.ws)
    }
  
    result.peerAddresses = []
    if (typeof result['x.pe'] === 'string' || Array.isArray(result['x.pe'])) {
      result.peerAddresses = result.peerAddresses.concat(result['x.pe'])
    }
  
    // remove duplicates by converting to Set and back
    result.announce = Array.from(new Set(result.announce))
    result.urlList = Array.from(new Set(result.urlList))
    result.peerAddresses = Array.from(new Set(result.peerAddresses))
  
    return result
  }
  
  function magnetURIEncode (obj) {
    obj = Object.assign({}, obj) // clone obj, so we can mutate it
  
    // support using convenience names, in addition to spec names
    // (example: `infoHash` for `xt`, `name` for `dn`)
    if (obj.infoHashBuffer) obj.xt = `urn:btih:${obj.infoHashBuffer.toString('hex')}`
    if (obj.infoHash) obj.xt = `urn:btih:${obj.infoHash}`
    if (obj.publicKeyBuffer) obj.xs = `urn:btpk:${obj.publicKeyBuffer.toString('hex')}`
    if (obj.publicKey) obj.xs = `urn:btpk:${obj.publicKey}`
    if (obj.name) obj.dn = obj.name
    if (obj.keywords) obj.kt = obj.keywords
    if (obj.announce) obj.tr = obj.announce
    if (obj.urlList) {
      obj.ws = obj.urlList
      delete obj.as
    }
    if (obj.peerAddresses) obj['x.pe'] = obj.peerAddresses
  
    let result = 'magnet:?'
    Object.keys(obj)
      .filter(key => key.length === 2 || key === 'x.pe')
      .forEach((key, i) => {
        const values = Array.isArray(obj[key]) ? obj[key] : [obj[key]]
        values.forEach((val, j) => {
          if ((i > 0 || j > 0) && ((key !== 'kt' && key !== 'so') || j === 0)) result += '&'
  
          if (key === 'dn') val = encodeURIComponent(val).replace(/%20/g, '+')
          if (key === 'tr' || key === 'as' || key === 'ws') {
            val = encodeURIComponent(val)
          }
          // Don't URI encode BEP46 keys
          if (key === 'xs' && !val.startsWith('urn:btpk:')) {
            val = encodeURIComponent(val)
          }
          if (key === 'kt') val = encodeURIComponent(val)
          if (key === 'so') return
  
          if (key === 'kt' && j > 0) result += `+${val}`
          else result += `${key}=${val}`
        })
        if (key === 'so') result += `${key}=${bep53Range.compose(values)}`
      })
  
    return result
  }
  