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
2017-07-22 11:10:37 +02:00
lbryuri . REGEXP _INVALID _URI = /[^A-Za-z0-9-]/g ;
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-06-06 06:21:55 +02:00
lbryuri . parse = function ( 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 (
"^((?: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 %s characters. ` ,
CHANNEL _NAME _MIN _LEN
)
) ;
}
contentName = path ;
}
2017-07-22 11:10:37 +02: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. ` ,
nameBadChars . length == 1 ? "" : "s" ,
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 %s. ` , modSep ) ) ;
}
if ( modSep == "#" ) {
claimId = modVal ;
} else if ( modSep == ":" ) {
claimSequence = modVal ;
} else if ( modSep == "$" ) {
bidPosition = modVal ;
}
}
if (
claimId &&
2017-06-28 17:24:16 +02:00
( claimId . length > CLAIM _ID _MAX _LEN || ! claimId . match ( /^[0-9a-f]+$/ ) ) &&
2017-06-28 21:54:33 +02: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]*$/ ) ) {
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." ) ) ;
}
2017-07-22 11:10:37 +02:00
const pathBadChars = path . match ( lbryuri . REGEXP _INVALID _URI ) ;
2017-06-20 14:08:52 +02:00
if ( pathBadChars ) {
throw new Error (
2017-06-29 00:08:16 +02:00
_ _ ( ` Invalid character in path: %s ` , pathBadChars . join ( ", " ) )
2017-06-20 14:08:52 +02:00
) ;
}
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 } : { } ) ,
} ;
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-06-06 06:21:55 +02:00
lbryuri . build = function ( uriObj , includeProto = true , allowExtraProps = false ) {
2017-06-20 14:08:52 +02:00
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 } ` : "" )
) ;
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-06-06 06:21:55 +02:00
lbryuri . normalize = function ( uri ) {
2017-06-17 19:59:18 +02:00
if ( uri . match ( /pending_claim/ ) ) return uri ;
2017-06-20 14:08:52 +02: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-05-12 22:49:15 +02:00
lbryuri . isValid = function ( uri ) {
2017-06-20 14:08:52 +02:00
let parts ;
try {
parts = lbryuri . parse ( lbryuri . normalize ( uri ) ) ;
} catch ( error ) {
return false ;
}
return parts && parts . name ;
2017-06-06 06:21:55 +02:00
} ;
lbryuri . isValidName = function ( name , checkCase = true ) {
2017-06-20 14:08:52 +02:00
const regexp = new RegExp ( "^[a-z0-9-]+$" , checkCase ? "" : "i" ) ;
return regexp . test ( name ) ;
2017-06-06 06:21:55 +02:00
} ;
2017-05-19 01:14:26 +02:00
2017-05-12 22:49:15 +02:00
lbryuri . isClaimable = function ( uri ) {
2017-06-20 14:08:52 +02:00
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-06-06 06:21:55 +02:00
} ;
2017-05-12 22:49:15 +02:00
2017-04-19 19:56:17 +02:00
window . lbryuri = lbryuri ;
2017-04-18 21:14:42 +02:00
export default lbryuri ;