2017-10-20 10:16:18 +02:00
const CHANNEL _NAME _MIN _LEN = 1 ;
2017-04-04 23:27:14 +02:00
const CLAIM _ID _MAX _LEN = 40 ;
2017-12-21 18:32:51 +01:00
const Lbryuri = { } ;
2017-04-04 23:27:14 +02:00
2017-12-21 18:32:51 +01:00
Lbryuri . REGEXP _INVALID _URI = /[^A-Za-z0-9-]/g ;
Lbryuri . REGEXP _ADDRESS = /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/ ;
2017-07-22 11:10:37 +02:00
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-12-28 00:48:11 +01:00
Lbryuri . parse = ( uri , requireProto = false ) => {
2017-06-20 14:08:52 +02:00
// Break into components. Empty sub-matches are converted to null
const componentsRegex = new RegExp (
2017-12-21 18:32:51 +01:00
'^((?: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-06-20 14:08:52 +02:00
) ;
const [ proto , name , modSep , modVal , pathSep , path ] = componentsRegex
. exec ( uri )
. slice ( 1 )
. map ( match => match || null ) ;
let contentName ;
// Validate protocol
if ( requireProto && ! proto ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'LBRY URIs must include a protocol prefix (lbry://).' ) ) ;
2017-06-20 14:08:52 +02:00
}
// Validate and process name
if ( ! name ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'URI does not include name.' ) ) ;
2017-06-20 14:08:52 +02:00
}
2017-12-21 18:32:51 +01:00
const isChannel = name . startsWith ( '@' ) ;
2017-06-20 14:08:52 +02:00
const channelName = isChannel ? name . slice ( 1 ) : name ;
if ( isChannel ) {
if ( ! channelName ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'No channel name after @.' ) ) ;
2017-06-20 14:08:52 +02:00
}
if ( channelName . length < CHANNEL _NAME _MIN _LEN ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( ` Channel names must be at least %s characters. ` , CHANNEL _NAME _MIN _LEN ) ) ;
2017-06-20 14:08:52 +02:00
}
contentName = path ;
}
2017-12-21 18:32:51 +01:00
const nameBadChars = ( channelName || name ) . match ( Lbryuri . REGEXP _INVALID _URI ) ;
2017-06-20 14:08:52 +02:00
if ( nameBadChars ) {
throw new Error (
_ _ (
` Invalid character %s in name: %s. ` ,
2017-12-21 18:32:51 +01:00
nameBadChars . length === 1 ? '' : 's' ,
nameBadChars . join ( ', ' )
2017-06-20 14:08:52 +02:00
)
) ;
}
// Validate and process modifier (claim ID, bid position or claim sequence)
2017-12-21 18:32:51 +01:00
let claimId ;
let claimSequence ;
let bidPosition ;
2017-06-20 14:08:52 +02:00
if ( modSep ) {
if ( ! modVal ) {
throw new Error ( _ _ ( ` No modifier provided after separator %s. ` , modSep ) ) ;
}
2017-12-21 18:32:51 +01:00
if ( modSep === '#' ) {
2017-06-20 14:08:52 +02:00
claimId = modVal ;
2017-12-21 18:32:51 +01:00
} else if ( modSep === ':' ) {
2017-06-20 14:08:52 +02:00
claimSequence = modVal ;
2017-12-21 18:32:51 +01:00
} else if ( modSep === '$' ) {
2017-06-20 14:08:52 +02:00
bidPosition = modVal ;
}
}
if (
claimId &&
2017-06-28 17:24:16 +02:00
( claimId . length > CLAIM _ID _MAX _LEN || ! claimId . match ( /^[0-9a-f]+$/ ) ) &&
2017-12-13 22:36:30 +01:00
! claimId . match ( /^pending/ ) // ought to be dropped when savePendingPublish drops hack
2017-06-20 14:08:52 +02:00
) {
throw new Error ( _ _ ( ` Invalid claim ID %s. ` , claimId ) ) ;
}
if ( claimSequence && ! claimSequence . match ( /^-?[1-9][0-9]*$/ ) ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'Claim sequence must be a number.' ) ) ;
2017-06-20 14:08:52 +02:00
}
if ( bidPosition && ! bidPosition . match ( /^-?[1-9][0-9]*$/ ) ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'Bid position must be a number.' ) ) ;
2017-06-20 14:08:52 +02:00
}
// Validate and process path
if ( path ) {
if ( ! isChannel ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'Only channel URIs may have a path.' ) ) ;
2017-06-20 14:08:52 +02:00
}
2017-12-21 18:32:51 +01:00
const pathBadChars = path . match ( Lbryuri . REGEXP _INVALID _URI ) ;
2017-06-20 14:08:52 +02:00
if ( pathBadChars ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( ` Invalid character in path: %s ` , pathBadChars . join ( ', ' ) ) ) ;
2017-06-20 14:08:52 +02:00
}
contentName = path ;
} else if ( pathSep ) {
2017-12-21 18:32:51 +01:00
throw new Error ( _ _ ( 'No path provided after /' ) ) ;
2017-06-20 14:08:52 +02:00
}
return {
name ,
path ,
isChannel ,
... ( contentName ? { contentName } : { } ) ,
... ( channelName ? { channelName } : { } ) ,
2017-12-21 18:32:51 +01:00
... ( claimSequence ? { claimSequence : parseInt ( claimSequence , 10 ) } : { } ) ,
... ( bidPosition ? { bidPosition : parseInt ( bidPosition , 10 ) } : { } ) ,
2017-06-20 14:08:52 +02:00
... ( claimId ? { claimId } : { } ) ,
... ( path ? { path } : { } ) ,
} ;
2017-06-06 06:21:55 +02:00
} ;
2017-04-04 23:27:14 +02:00
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 .
* /
2017-12-28 00:48:11 +01:00
Lbryuri . build = ( uriObj , includeProto = true ) => {
2017-12-21 18:32:51 +01:00
const { claimId , claimSequence , bidPosition , contentName , channelName } = uriObj ;
let { name , path } = uriObj ;
2017-06-20 14:08:52 +02:00
if ( channelName ) {
2017-12-21 18:32:51 +01:00
const channelNameFormatted = channelName . startsWith ( '@' ) ? channelName : ` @ ${ channelName } ` ;
2017-06-20 14:08:52 +02:00
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 (
_ _ (
2017-12-21 18:32:51 +01:00
'Path and contentName do not match. Only one is required; most likely you wanted contentName.'
2017-06-20 14:08:52 +02:00
)
) ;
}
}
return (
2017-12-21 18:32:51 +01:00
( includeProto ? 'lbry://' : '' ) +
2017-06-20 14:08:52 +02:00
name +
2017-12-21 18:32:51 +01:00
( claimId ? ` # ${ claimId } ` : '' ) +
( claimSequence ? ` : ${ claimSequence } ` : '' ) +
( bidPosition ? ` ${ bidPosition } ` : '' ) +
( path ? ` / ${ path } ` : '' )
2017-06-20 14:08:52 +02:00
) ;
2017-06-06 06:21:55 +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-12-28 00:48:11 +01:00
Lbryuri . normalize = uri => {
2017-06-17 19:59:18 +02:00
if ( uri . match ( /pending_claim/ ) ) return uri ;
2017-12-21 18:32:51 +01:00
const { name , path , bidPosition , claimSequence , claimId } = Lbryuri . parse ( uri ) ;
return Lbryuri . build ( { name , path , claimSequence , bidPosition , claimId } ) ;
2017-06-06 06:21:55 +02:00
} ;
2017-04-11 03:18:58 +02:00
2017-12-28 00:48:11 +01:00
Lbryuri . isValid = uri => {
2017-06-20 14:08:52 +02:00
let parts ;
try {
2017-12-21 18:32:51 +01:00
parts = Lbryuri . parse ( Lbryuri . normalize ( uri ) ) ;
2017-06-20 14:08:52 +02:00
} catch ( error ) {
return false ;
}
return parts && parts . name ;
2017-06-06 06:21:55 +02:00
} ;
2017-12-28 00:48:11 +01:00
Lbryuri . isValidName = ( name , checkCase = true ) => {
2017-12-21 18:32:51 +01:00
const regexp = new RegExp ( '^[a-z0-9-]+$' , checkCase ? '' : 'i' ) ;
2017-06-20 14:08:52 +02:00
return regexp . test ( name ) ;
2017-06-06 06:21:55 +02:00
} ;
2017-05-19 01:14:26 +02:00
2017-12-28 00:48:11 +01:00
Lbryuri . isClaimable = uri => {
2017-06-20 14:08:52 +02:00
let parts ;
try {
2017-12-21 18:32:51 +01:00
parts = Lbryuri . parse ( Lbryuri . normalize ( uri ) ) ;
2017-06-20 14:08:52 +02:00
} catch ( error ) {
return false ;
}
return (
parts &&
parts . name &&
! parts . claimId &&
! parts . bidPosition &&
! parts . claimSequence &&
! parts . isChannel &&
! parts . path
) ;
2017-06-06 06:21:55 +02:00
} ;
2017-05-12 22:49:15 +02:00
2017-12-21 18:32:51 +01:00
window . lbryuri = Lbryuri ;
export default Lbryuri ;