lbry-desktop/ui/js/lbryuri.js
2017-05-12 16:49:15 -04:00

190 lines
6.1 KiB
JavaScript

const CHANNEL_NAME_MIN_LEN = 4;
const CLAIM_ID_MAX_LEN = 40;
const lbryuri = {};
/**
* Parses a LBRY name into its component parts. Throws errors with user-friendly
* messages for invalid names.
*
* N.B. that "name" indicates the value in the name position of the URI. For
* claims for channel content, this will actually be the channel name, and
* the content name is in the path (e.g. lbry://@channel/content)
*
* In most situations, you'll want to use the contentName and channelName keys
* and ignore the name key.
*
* Returns a dictionary with keys:
* - name (string): The value in the "name" position in the URI. Note that this
* could be either content name or channel name; see above.
* - path (string, if persent)
* - claimSequence (int, if present)
* - bidPosition (int, if present)
* - claimId (string, if present)
* - isChannel (boolean)
* - contentName (string): For anon claims, the name; for channel claims, the path
* - channelName (string, if present): Channel name without @
*/
lbryuri.parse = function(uri, requireProto=false) {
// Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp(
'^((?:lbry:\/\/)?)' + // protocol
'([^:$#/]*)' + // name (stops at the first separator or end)
'([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end)
'(/?)(.*)' // path separator, path
);
const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null);
let contentName;
// Validate protocol
if (requireProto && !proto) {
throw new Error('LBRY URIs must include a protocol prefix (lbry://).');
}
// Validate and process name
if (!name) {
throw new Error('URI does not include name.');
}
const isChannel = name.startsWith('@');
const channelName = isChannel ? name.slice(1) : name;
if (isChannel) {
if (!channelName) {
throw new Error('No channel name after @.');
}
if (channelName.length < CHANNEL_NAME_MIN_LEN) {
throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`);
}
contentName = path;
}
const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g);
if (nameBadChars) {
throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`);
}
// Validate and process modifier (claim ID, bid position or claim sequence)
let claimId, claimSequence, bidPosition;
if (modSep) {
if (!modVal) {
throw new Error(`No modifier provided after separator ${modSep}.`);
}
if (modSep == '#') {
claimId = modVal;
} else if (modSep == ':') {
claimSequence = modVal;
} else if (modSep == '$') {
bidPosition = modVal;
}
}
if (claimId && (claimId.length > CLAIM_ID_MAX_LEN || !claimId.match(/^[0-9a-f]+$/))) {
throw new Error(`Invalid claim ID ${claimId}.`);
}
if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
throw new Error('Claim sequence must be a number.');
}
if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
throw new Error('Bid position must be a number.');
}
// Validate and process path
if (path) {
if (!isChannel) {
throw new Error('Only channel URIs may have a path.');
}
const pathBadChars = path.match(/[^A-Za-z0-9-]/g);
if (pathBadChars) {
throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`);
}
contentName = path;
} else if (pathSep) {
throw new Error('No path provided after /');
}
return {
name, path, isChannel,
... contentName ? {contentName} : {},
... channelName ? {channelName} : {},
... claimSequence ? {claimSequence: parseInt(claimSequence)} : {},
... bidPosition ? {bidPosition: parseInt(bidPosition)} : {},
... claimId ? {claimId} : {},
... path ? {path} : {},
};
}
/**
* Takes an object in the same format returned by lbryuri.parse() and builds a URI.
*
* The channelName key will accept names with or without the @ prefix.
*/
lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) {
let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj;
if (channelName) {
const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName;
if (!name) {
name = channelNameFormatted;
} else if (name !== channelNameFormatted) {
throw new Error('Received a channel content URI, but name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.');
}
}
if (contentName) {
if (!name) {
name = contentName;
} else if (!path) {
path = contentName;
}
if (path && path !== contentName) {
throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.');
}
}
return (includeProto ? 'lbry://' : '') + name +
(claimId ? `#${claimId}` : '') +
(claimSequence ? `:${claimSequence}` : '') +
(bidPosition ? `\$${bidPosition}` : '') +
(path ? `/${path}` : '');
}
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of adding the lbry:// prefix if needed) */
lbryuri.normalize= function(uri) {
const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri);
return lbryuri.build({name, path, claimSequence, bidPosition, claimId});
}
lbryuri.isValid = function(uri) {
let parts
try {
parts = lbryuri.parse(lbryuri.normalize(uri))
} catch (error) {
return false;
}
return parts && parts.name;
}
lbryuri.isClaimable = function(uri) {
let parts
try {
parts = lbryuri.parse(lbryuri.normalize(uri))
} catch (error) {
return false;
}
return parts && parts.name && !parts.claimId && !parts.bidPosition && !parts.claimSequence && !parts.isChannel && !parts.path;
}
window.lbryuri = lbryuri;
export default lbryuri;