2017-04-04 23:27:14 +02:00
const CHANNEL _NAME _MIN _LEN = 4 ;
const CLAIM _ID _MAX _LEN = 40 ;
2017-04-18 21:14:42 +02:00
const lbryuri = { } ;
2017-04-04 23:27:14 +02:00
/ * *
* Parses a LBRY name into its component parts . Throws errors with user - friendly
* messages for invalid names .
*
2017-04-19 19:56:17 +02:00
* 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 .
*
2017-04-04 23:27:14 +02:00
* Returns a dictionary with keys :
2017-04-19 19:56:17 +02:00
* - 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 )
2017-04-04 23:27:14 +02:00
* - claimSequence ( int , if present )
* - bidPosition ( int , if present )
* - claimId ( string , if present )
2017-04-19 19:56:17 +02:00
* - isChannel ( boolean )
* - contentName ( string ) : For anon claims , the name ; for channel claims , the path
* - channelName ( string , if present ) : Channel name without @
2017-04-04 23:27:14 +02:00
* /
2017-04-18 21:14:42 +02:00
lbryuri . parse = function ( uri , requireProto = false ) {
2017-04-04 23:27:14 +02:00
// 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
) ;
2017-04-18 21:14:42 +02:00
const [ proto , name , modSep , modVal , pathSep , path ] = componentsRegex . exec ( uri ) . slice ( 1 ) . map ( match => match || null ) ;
2017-04-04 23:27:14 +02:00
2017-04-19 19:56:17 +02:00
let contentName ;
2017-04-04 23:27:14 +02:00
// 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.' ) ;
}
2017-04-19 19:56:17 +02:00
const isChannel = name . startsWith ( '@' ) ;
const channelName = isChannel ? name . slice ( 1 ) : name ;
2017-04-04 23:27:14 +02:00
if ( isChannel ) {
2017-04-19 19:56:17 +02:00
if ( ! channelName ) {
2017-04-04 23:27:14 +02:00
throw new Error ( 'No channel name after @.' ) ;
}
2017-04-19 19:56:17 +02:00
if ( channelName . length < CHANNEL _NAME _MIN _LEN ) {
2017-04-04 23:27:14 +02:00
throw new Error ( ` Channel names must be at least ${ CHANNEL _NAME _MIN _LEN } characters. ` ) ;
}
2017-04-19 19:56:17 +02:00
contentName = path ;
2017-04-04 23:27:14 +02:00
}
2017-04-19 19:56:17 +02:00
const nameBadChars = ( channelName || name ) . match ( /[^A-Za-z0-9-]/g ) ;
2017-04-04 23:27:14 +02:00
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 } . ` ) ;
}
2017-04-11 03:18:37 +02:00
if ( claimSequence && ! claimSequence . match ( /^-?[1-9][0-9]*$/ ) ) {
throw new Error ( 'Claim sequence must be a number.' ) ;
2017-04-04 23:27:14 +02:00
}
2017-04-11 03:18:37 +02:00
if ( bidPosition && ! bidPosition . match ( /^-?[1-9][0-9]*$/ ) ) {
throw new Error ( 'Bid position must be a number.' ) ;
2017-04-04 23:27:14 +02:00
}
2017-04-19 19:56:17 +02:00
// Validate and process path
2017-04-04 23:27:14 +02:00
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 ( ', ' ) } ` ) ;
}
2017-04-19 19:56:17 +02:00
contentName = path ;
2017-04-04 23:27:14 +02:00
} else if ( pathSep ) {
throw new Error ( 'No path provided after /' ) ;
}
return {
2017-04-19 19:56:17 +02:00
name , path , isChannel ,
... contentName ? { contentName } : { } ,
... channelName ? { channelName } : { } ,
2017-04-04 23:27:14 +02:00
... claimSequence ? { claimSequence : parseInt ( claimSequence ) } : { } ,
... bidPosition ? { bidPosition : parseInt ( bidPosition ) } : { } ,
... claimId ? { claimId } : { } ,
... path ? { path } : { } ,
} ;
}
2017-04-19 19:56:17 +02:00
/ * *
* 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 ) {
2017-04-21 01:26:29 +02:00
if ( ! name ) {
name = contentName ;
} else if ( ! path ) {
2017-04-19 19:56:17 +02:00
path = contentName ;
2017-04-21 01:26:29 +02:00
}
if ( path && path !== contentName ) {
2017-04-19 19:56:17 +02:00
throw new Error ( 'path and contentName do not match. Only one is required; most likely you wanted contentName.' ) ;
}
}
2017-04-04 23:27:14 +02:00
return ( includeProto ? 'lbry://' : '' ) + name +
( claimId ? ` # ${ claimId } ` : '' ) +
( claimSequence ? ` : ${ claimSequence } ` : '' ) +
( bidPosition ? ` \$ ${ bidPosition } ` : '' ) +
( path ? ` / ${ path } ` : '' ) ;
2017-04-19 19:56:17 +02:00
2017-04-04 23:27:14 +02:00
}
2017-04-11 03:18:58 +02:00
/ * T a k e s a p a r s e a b l e L B R Y U R I a n d c o n v e r t s i t t o s t a n d a r d , c a n o n i c a l f o r m a t ( c u r r e n t l y t h i s j u s t
2017-04-19 19:56:17 +02:00
* consists of adding the lbry : // prefix if needed) */
2017-04-18 21:14:42 +02:00
lbryuri . normalize = function ( uri ) {
2017-04-19 19:56:17 +02:00
const { name , path , bidPosition , claimSequence , claimId } = lbryuri . parse ( uri ) ;
return lbryuri . build ( { name , path , claimSequence , bidPosition , claimId } ) ;
2017-04-11 03:18:58 +02:00
}
2017-05-12 22:49:15 +02:00
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 ;
}
2017-04-19 19:56:17 +02:00
window . lbryuri = lbryuri ;
2017-04-18 21:14:42 +02:00
export default lbryuri ;