From 4bdcd008c70369b841745403c591096be6d2ef50 Mon Sep 17 00:00:00 2001
From: Thomas Zarebczan <thomas.zarebczan@gmail.com>
Date: Wed, 4 Mar 2020 23:16:17 -0500
Subject: [PATCH] Ad isValid to parseURI

---
 dist/bundle.es.js             | 107 +++++++++++++++++-----------------
 dist/flow-typed/lbryURI.js    |   3 +-
 flow-typed/lbryURI.js         |   3 +-
 src/lbryURI.js                | 102 ++++++++++++++++++--------------
 src/redux/reducers/claims.js  |   2 +-
 src/redux/selectors/claims.js |  46 +++++++--------
 src/redux/selectors/search.js |  26 ++++-----
 7 files changed, 148 insertions(+), 141 deletions(-)

diff --git a/dist/bundle.es.js b/dist/bundle.es.js
index d9c16c3..5ebb021 100644
--- a/dist/bundle.es.js
+++ b/dist/bundle.es.js
@@ -1168,7 +1168,9 @@ const separateQuerystring = new RegExp(queryStringBreaker);
  * messages for invalid names.
  *
  * Returns a dictionary with keys:
+
  *   - path (string)
+ *   - isValid (boolean)
  *   - isChannel (boolean)
  *   - streamName (string, if present)
  *   - streamClaimId (string, if present)
@@ -1200,18 +1202,21 @@ function parseURI(URL, requireProto = false) {
   const [proto, ...rest] = regexMatch.slice(1).map(match => match || null);
   const path = rest.join('');
   const [streamNameOrChannelName, primaryModSeparator, primaryModValue, pathSep, possibleStreamName, secondaryModSeparator, secondaryModValue] = rest;
-
+  let isValid = true;
   // Validate protocol
   if (requireProto && !proto) {
-    throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).'));
+    isValid = false;
+    console.log('LBRY URLs must include a protocol prefix (lbry://).');
   }
 
   // Validate and process name
   if (!streamNameOrChannelName) {
-    throw new Error(__('URL does not include name.'));
+    isValid = false;
+    console.log('URL does not include name.');
   }
 
   rest.forEach(urlPiece => {
+    isValid = false;
     if (urlPiece && urlPiece.includes(' ')) {
       console.error('URL can not include a space');
     }
@@ -1223,24 +1228,30 @@ function parseURI(URL, requireProto = false) {
 
   if (includesChannel) {
     if (!channelName) {
-      throw new Error(__('No channel name after @.'));
+      isValid = false;
+      console.log('No channel name after @.');
     }
 
     if (channelName.length < channelNameMinLength) {
-      throw new Error(__(`Channel names must be at least %channelNameMinLength% characters.`, {
+      isValid = false;
+      console.log(`Channel names must be at least %channelNameMinLength% characters.`, {
         channelNameMinLength
-      }));
+      });
     }
   }
 
   // Validate and process modifier
-  const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(primaryModSeparator, primaryModValue);
-  const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(secondaryModSeparator, secondaryModValue);
+  const [primaryClaimId, primaryClaimSequence, primaryBidPosition, primaryValid] = parseURIModifier(primaryModSeparator, primaryModValue);
+  const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition, secondaryValid] = parseURIModifier(secondaryModSeparator, secondaryModValue);
+
+  if (primaryModSeparator && !primaryValid || secondaryModSeparator && !secondaryValid) isValid = false;
+
   const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
   const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
   const channelClaimId = includesChannel && primaryClaimId;
 
   return _extends({
+    isValid,
     isChannel,
     path
   }, streamName ? { streamName } : {}, streamClaimId ? { streamClaimId } : {}, channelName ? { channelName } : {}, channelClaimId ? { channelClaimId } : {}, primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}, secondaryClaimSequence ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } : {}, primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}, secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}, {
@@ -1256,10 +1267,12 @@ function parseURIModifier(modSeperator, modValue) {
   let claimId;
   let claimSequence;
   let bidPosition;
+  let isValid = true;
 
   if (modSeperator) {
     if (!modValue) {
-      throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator }));
+      isValid = false;
+      console.log(`No modifier provided after separator %modSeperator%.`, { modSeperator });
     }
 
     if (modSeperator === '#') {
@@ -1272,18 +1285,21 @@ function parseURIModifier(modSeperator, modValue) {
   }
 
   if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) {
-    throw new Error(__(`Invalid claim ID %claimId%.`, { claimId }));
+    isValid = false;
+    console.log(`Invalid claim ID %claimId%.`, { claimId });
   }
 
   if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
-    throw new Error(__('Claim sequence must be a number.'));
+    isValid = false;
+    console.log('Claim sequence must be a number.');
   }
 
   if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
-    throw new Error(__('Bid position must be a number.'));
+    isValid = false;
+    console.log('Bid position must be a number.');
   }
 
-  return [claimId, claimSequence, bidPosition];
+  return [claimId, claimSequence, bidPosition, isValid];
 }
 
 /**
@@ -1324,6 +1340,7 @@ function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') {
 /* Takes a parseable LBRY URL and converts it to standard, canonical format */
 function normalizeURI(URL) {
   const {
+    isValid,
     streamName,
     streamClaimId,
     channelName,
@@ -1334,7 +1351,7 @@ function normalizeURI(URL) {
     secondaryBidPosition
   } = parseURI(URL);
 
-  return buildURI({
+  return isValid && buildURI({
     streamName,
     streamClaimId,
     channelName,
@@ -1347,13 +1364,9 @@ function normalizeURI(URL) {
 }
 
 function isURIValid(URL) {
-  try {
-    parseURI(normalizeURI(URL));
-  } catch (error) {
-    return false;
-  }
-
-  return true;
+  let isValid;
+  ({ isValid } = parseURI(normalizeURI(URL)));
+  return isValid;
 }
 
 function isNameValid(claimName) {
@@ -1361,14 +1374,9 @@ function isNameValid(claimName) {
 }
 
 function isURIClaimable(URL) {
-  let parts;
-  try {
-    parts = parseURI(normalizeURI(URL));
-  } catch (error) {
-    return false;
-  }
+  const { isValid, parts } = parseURI(normalizeURI(URL));
 
-  return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
+  return isValid && parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
 }
 
 function convertToShareLink(URL) {
@@ -1445,8 +1453,8 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec
   }
 
   let searchSuggestions = [];
-  try {
-    const uri = normalizeURI(query);
+  const uri = normalizeURI(query);
+  if (uri) {
     const { channelName, streamName, isChannel } = parseURI(uri);
     searchSuggestions.push({
       value: query,
@@ -1456,7 +1464,7 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec
       shorthand: isChannel ? channelName : streamName,
       type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE
     });
-  } catch (e) {
+  } else {
     searchSuggestions.push({
       value: query,
       type: SEARCH_TYPES.SEARCH
@@ -1472,16 +1480,16 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec
   if (apiSuggestions.length) {
     searchSuggestions = searchSuggestions.concat(apiSuggestions.filter(suggestion => suggestion !== query).map(suggestion => {
       // determine if it's a channel
-      try {
-        const uri = normalizeURI(suggestion);
-        const { channelName, streamName, isChannel } = parseURI(uri);
+      const uri = normalizeURI(suggestion);
+      if (uri) {
+        const { isValid, channelName, streamName, isChannel } = parseURI(uri);
 
         return {
           value: uri,
           shorthand: isChannel ? channelName : streamName,
           type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE
         };
-      } catch (e) {
+      } else {
         // search result includes some character that isn't valid in claim names
         return {
           value: suggestion,
@@ -2031,10 +2039,10 @@ const selectPendingClaims = reselect.createSelector(selectState$2, state => Obje
 const makeSelectClaimIsPending = uri => reselect.createSelector(selectPendingById, pendingById => {
   let claimId;
 
-  try {
-    const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
+  const { isValid, isChannel, channelClaimId, streamClaimId } = parseURI(uri);
+  if (isValid) {
     claimId = isChannel ? channelClaimId : streamClaimId;
-  } catch (e) {}
+  }
 
   if (claimId) {
     return Boolean(pendingById[claimId]);
@@ -2051,16 +2059,13 @@ const makeSelectClaimForUri = (uri, returnRepost = true) => reselect.createSelec
   // Check if a claim is pending first
   // It won't be in claimsByUri because resolving it will return nothing
 
-  let valid;
   let channelClaimId;
   let streamClaimId;
   let isChannel;
-  try {
-    ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri));
-    valid = true;
-  } catch (e) {}
+  let isValid;
+  ({ isValid, isChannel, channelClaimId, streamClaimId } = parseURI(uri));
 
-  if (valid && byUri) {
+  if (isValid && byUri) {
     const claimId = isChannel ? channelClaimId : streamClaimId;
     const pendingClaim = pendingById[claimId];
 
@@ -2096,18 +2101,12 @@ const selectMyActiveClaims = reselect.createSelector(selectMyClaimsRaw, selectAb
 
 const makeSelectClaimIsMine = rawUri => {
   let uri;
-  try {
-    uri = normalizeURI(rawUri);
-  } catch (e) {}
+  uri = normalizeURI(rawUri);
 
-  return reselect.createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
-    try {
-      parseURI(uri);
-    } catch (e) {
-      return false;
-    }
+  return uri && reselect.createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
+    const { isValid } = parseURI(uri);
 
-    return claims && claims[uri] && claims[uri].claim_id && myClaims.has(claims[uri].claim_id);
+    return isValid && claims && claims[uri] && claims[uri].claim_id && myClaims.has(claims[uri].claim_id);
   });
 };
 
diff --git a/dist/flow-typed/lbryURI.js b/dist/flow-typed/lbryURI.js
index 4365da3..7ce393b 100644
--- a/dist/flow-typed/lbryURI.js
+++ b/dist/flow-typed/lbryURI.js
@@ -1,7 +1,8 @@
 // @flow
 declare type LbryUrlObj = {
-  // Path and channel will always exist when calling parseURI
+  // Path, channel, and isValid will always exist when calling parseURI
   // But they may not exist when code calls buildURI
+  isValid?: boolean,
   isChannel?: boolean,
   path?: string,
   streamName?: string,
diff --git a/flow-typed/lbryURI.js b/flow-typed/lbryURI.js
index 4365da3..7ce393b 100644
--- a/flow-typed/lbryURI.js
+++ b/flow-typed/lbryURI.js
@@ -1,7 +1,8 @@
 // @flow
 declare type LbryUrlObj = {
-  // Path and channel will always exist when calling parseURI
+  // Path, channel, and isValid will always exist when calling parseURI
   // But they may not exist when code calls buildURI
+  isValid?: boolean,
   isChannel?: boolean,
   path?: string,
   streamName?: string,
diff --git a/src/lbryURI.js b/src/lbryURI.js
index b63c1f2..0adee65 100644
--- a/src/lbryURI.js
+++ b/src/lbryURI.js
@@ -17,7 +17,9 @@ const separateQuerystring = new RegExp(queryStringBreaker);
  * messages for invalid names.
  *
  * Returns a dictionary with keys:
+
  *   - path (string)
+ *   - isValid (boolean)
  *   - isChannel (boolean)
  *   - streamName (string, if present)
  *   - streamClaimId (string, if present)
@@ -60,18 +62,21 @@ export function parseURI(URL: string, requireProto: boolean = false): LbryUrlObj
     secondaryModSeparator,
     secondaryModValue,
   ] = rest;
-
+  let isValid = true;
   // Validate protocol
   if (requireProto && !proto) {
-    throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).'));
+    isValid = false;
+    console.log('LBRY URLs must include a protocol prefix (lbry://).');
   }
 
   // Validate and process name
   if (!streamNameOrChannelName) {
-    throw new Error(__('URL does not include name.'));
+    isValid = false;
+    console.log('URL does not include name.');
   }
 
   rest.forEach(urlPiece => {
+    isValid = false;
     if (urlPiece && urlPiece.includes(' ')) {
       console.error('URL can not include a space');
     }
@@ -83,32 +88,39 @@ export function parseURI(URL: string, requireProto: boolean = false): LbryUrlObj
 
   if (includesChannel) {
     if (!channelName) {
-      throw new Error(__('No channel name after @.'));
+      isValid = false;
+      console.log('No channel name after @.');
     }
 
     if (channelName.length < channelNameMinLength) {
-      throw new Error(
-        __(`Channel names must be at least %channelNameMinLength% characters.`, {
-          channelNameMinLength,
-        })
-      );
+      isValid = false;
+      console.log(`Channel names must be at least %channelNameMinLength% characters.`, {
+        channelNameMinLength,
+      });
     }
   }
 
   // Validate and process modifier
-  const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier(
+  const [primaryClaimId, primaryClaimSequence, primaryBidPosition, primaryValid] = parseURIModifier(
     primaryModSeparator,
     primaryModValue
   );
-  const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier(
-    secondaryModSeparator,
-    secondaryModValue
-  );
+  const [
+    secondaryClaimId,
+    secondaryClaimSequence,
+    secondaryBidPosition,
+    secondaryValid,
+  ] = parseURIModifier(secondaryModSeparator, secondaryModValue);
+
+  if ((primaryModSeparator && !primaryValid) || (secondaryModSeparator && !secondaryValid))
+    isValid = false;
+
   const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName;
   const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId;
   const channelClaimId = includesChannel && primaryClaimId;
 
   return {
+    isValid,
     isChannel,
     path,
     ...(streamName ? { streamName } : {}),
@@ -135,10 +147,12 @@ function parseURIModifier(modSeperator: ?string, modValue: ?string) {
   let claimId;
   let claimSequence;
   let bidPosition;
+  let isValid = true;
 
   if (modSeperator) {
     if (!modValue) {
-      throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator }));
+      isValid = false;
+      console.log(`No modifier provided after separator %modSeperator%.`, { modSeperator });
     }
 
     if (modSeperator === '#') {
@@ -151,18 +165,21 @@ function parseURIModifier(modSeperator: ?string, modValue: ?string) {
   }
 
   if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) {
-    throw new Error(__(`Invalid claim ID %claimId%.`, { claimId }));
+    isValid = false;
+    console.log(`Invalid claim ID %claimId%.`, { claimId });
   }
 
   if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) {
-    throw new Error(__('Claim sequence must be a number.'));
+    isValid = false;
+    console.log('Claim sequence must be a number.');
   }
 
   if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) {
-    throw new Error(__('Bid position must be a number.'));
+    isValid = false;
+    console.log('Bid position must be a number.');
   }
 
-  return [claimId, claimSequence, bidPosition];
+  return [claimId, claimSequence, bidPosition, isValid];
 }
 
 /**
@@ -240,6 +257,7 @@ export function buildURI(
 /* Takes a parseable LBRY URL and converts it to standard, canonical format */
 export function normalizeURI(URL: string) {
   const {
+    isValid,
     streamName,
     streamClaimId,
     channelName,
@@ -250,41 +268,35 @@ export function normalizeURI(URL: string) {
     secondaryBidPosition,
   } = parseURI(URL);
 
-  return buildURI({
-    streamName,
-    streamClaimId,
-    channelName,
-    channelClaimId,
-    primaryClaimSequence,
-    primaryBidPosition,
-    secondaryClaimSequence,
-    secondaryBidPosition,
-  });
+  return (
+    isValid &&
+    buildURI({
+      streamName,
+      streamClaimId,
+      channelName,
+      channelClaimId,
+      primaryClaimSequence,
+      primaryBidPosition,
+      secondaryClaimSequence,
+      secondaryBidPosition,
+    })
+  );
 }
 
-export function isURIValid(URL: string): boolean {
-  try {
-    parseURI(normalizeURI(URL));
-  } catch (error) {
-    return false;
-  }
-
-  return true;
+export function isURIValid(URL: string) {
+  let isValid;
+  ({ isValid } = parseURI(normalizeURI(URL)));
+  return isValid;
 }
 
 export function isNameValid(claimName: string) {
   return !regexInvalidURI.test(claimName);
 }
 
-export function isURIClaimable(URL: string) {
-  let parts;
-  try {
-    parts = parseURI(normalizeURI(URL));
-  } catch (error) {
-    return false;
-  }
+export function isURIClaimable(URL: string): boolean {
+  const { isValid, parts } = parseURI(normalizeURI(URL));
 
-  return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
+  return isValid && parts && parts.streamName && !parts.streamClaimId && !parts.isChannel;
 }
 
 export function convertToShareLink(URL: string) {
diff --git a/src/redux/reducers/claims.js b/src/redux/reducers/claims.js
index 7ca9745..b6288f4 100644
--- a/src/redux/reducers/claims.js
+++ b/src/redux/reducers/claims.js
@@ -9,7 +9,7 @@
 // - Sean
 
 import * as ACTIONS from 'constants/action_types';
-import { buildURI, parseURI } from 'lbryURI';
+import { buildURI } from 'lbryURI';
 import { concatClaims } from 'util/claim';
 
 type State = {
diff --git a/src/redux/selectors/claims.js b/src/redux/selectors/claims.js
index 4db130a..665aff1 100644
--- a/src/redux/selectors/claims.js
+++ b/src/redux/selectors/claims.js
@@ -86,10 +86,10 @@ export const makeSelectClaimIsPending = (uri: string) =>
     pendingById => {
       let claimId;
 
-      try {
-        const { isChannel, channelClaimId, streamClaimId } = parseURI(uri);
+      const { isValid, isChannel, channelClaimId, streamClaimId } = parseURI(uri);
+      if (isValid) {
         claimId = isChannel ? channelClaimId : streamClaimId;
-      } catch (e) {}
+      }
 
       if (claimId) {
         return Boolean(pendingById[claimId]);
@@ -115,16 +115,13 @@ export const makeSelectClaimForUri = (uri: string, returnRepost: boolean = true)
       // Check if a claim is pending first
       // It won't be in claimsByUri because resolving it will return nothing
 
-      let valid;
       let channelClaimId;
       let streamClaimId;
       let isChannel;
-      try {
-        ({ isChannel, channelClaimId, streamClaimId } = parseURI(uri));
-        valid = true;
-      } catch (e) {}
+      let isValid;
+      ({ isValid, isChannel, channelClaimId, streamClaimId } = parseURI(uri));
 
-      if (valid && byUri) {
+      if (isValid && byUri) {
         const claimId = isChannel ? channelClaimId : streamClaimId;
         const pendingClaim = pendingById[claimId];
 
@@ -178,22 +175,25 @@ export const selectMyActiveClaims = createSelector(
 
 export const makeSelectClaimIsMine = (rawUri: string) => {
   let uri;
-  try {
-    uri = normalizeURI(rawUri);
-  } catch (e) {}
+  uri = normalizeURI(rawUri);
 
-  return createSelector(
-    selectClaimsByUri,
-    selectMyActiveClaims,
-    (claims, myClaims) => {
-      try {
-        parseURI(uri);
-      } catch (e) {
-        return false;
+  return (
+    uri &&
+    createSelector(
+      selectClaimsByUri,
+      selectMyActiveClaims,
+      (claims, myClaims) => {
+        const { isValid } = parseURI(uri);
+
+        return (
+          isValid &&
+          claims &&
+          claims[uri] &&
+          claims[uri].claim_id &&
+          myClaims.has(claims[uri].claim_id)
+        );
       }
-
-      return claims && claims[uri] && claims[uri].claim_id && myClaims.has(claims[uri].claim_id);
-    }
+    )
   );
 };
 
diff --git a/src/redux/selectors/search.js b/src/redux/selectors/search.js
index e3efd11..38837aa 100644
--- a/src/redux/selectors/search.js
+++ b/src/redux/selectors/search.js
@@ -106,8 +106,8 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
     }
 
     let searchSuggestions = [];
-    try {
-      const uri = normalizeURI(query);
+    const uri = normalizeURI(query);
+    if (uri) {
       const { channelName, streamName, isChannel } = parseURI(uri);
       searchSuggestions.push(
         {
@@ -120,7 +120,7 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
           type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
         }
       );
-    } catch (e) {
+    } else {
       searchSuggestions.push({
         value: query,
         type: SEARCH_TYPES.SEARCH,
@@ -139,16 +139,16 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
           .filter(suggestion => suggestion !== query)
           .map(suggestion => {
             // determine if it's a channel
-            try {
-              const uri = normalizeURI(suggestion);
-              const { channelName, streamName, isChannel } = parseURI(uri);
+            const uri = normalizeURI(suggestion);
+            if (uri) {
+              const { isValid, channelName, streamName, isChannel } = parseURI(uri);
 
               return {
                 value: uri,
                 shorthand: isChannel ? channelName : streamName,
                 type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
               };
-            } catch (e) {
+            } else {
               // search result includes some character that isn't valid in claim names
               return {
                 value: suggestion,
@@ -172,21 +172,15 @@ type CustomOptions = {
   from?: number,
   related_to?: string,
   nsfw?: boolean,
-}
+};
 
-export const makeSelectQueryWithOptions = (
-  customQuery: ?string,
-  options: CustomOptions,
-) =>
+export const makeSelectQueryWithOptions = (customQuery: ?string, options: CustomOptions) =>
   createSelector(
     selectSearchValue,
     selectSearchOptions,
     (query, defaultOptions) => {
       const searchOptions = { ...defaultOptions, ...options };
-      const queryString = getSearchQueryString(
-        customQuery || query,
-        searchOptions,
-      );
+      const queryString = getSearchQueryString(customQuery || query, searchOptions);
 
       return queryString;
     }