spotify_id.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. use std;
  2. use std::fmt;
  3. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  4. pub enum SpotifyAudioType {
  5. Track,
  6. Podcast,
  7. NonPlayable,
  8. }
  9. impl SpotifyAudioType {
  10. fn from_str(src: &str) -> SpotifyAudioType {
  11. match src {
  12. "track" => SpotifyAudioType::Track,
  13. "episode" => SpotifyAudioType::Podcast,
  14. _ => SpotifyAudioType::NonPlayable,
  15. }
  16. }
  17. fn to_str(self) -> &'static str {
  18. match self {
  19. SpotifyAudioType::Track => "track",
  20. SpotifyAudioType::Podcast => "episode",
  21. SpotifyAudioType::NonPlayable => "unknown",
  22. }
  23. }
  24. fn len(self) -> usize {
  25. match self {
  26. SpotifyAudioType::Track => 5,
  27. SpotifyAudioType::Podcast => 7,
  28. SpotifyAudioType::NonPlayable => 7,
  29. }
  30. }
  31. }
  32. impl std::convert::From<&str> for SpotifyAudioType {
  33. fn from(v: &str) -> Self {
  34. SpotifyAudioType::from_str(v)
  35. }
  36. }
  37. impl std::convert::Into<&str> for SpotifyAudioType {
  38. fn into(self) -> &'static str {
  39. self.to_str()
  40. }
  41. }
  42. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
  43. pub struct SpotifyId {
  44. pub id: u128,
  45. pub audio_type: SpotifyAudioType,
  46. }
  47. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
  48. pub struct SpotifyIdError;
  49. const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  50. const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef";
  51. impl SpotifyId {
  52. const SIZE: usize = 16;
  53. const SIZE_BASE16: usize = 32;
  54. const SIZE_BASE62: usize = 22;
  55. fn as_track(n: u128) -> SpotifyId {
  56. SpotifyId {
  57. id: n,
  58. audio_type: SpotifyAudioType::Track,
  59. }
  60. }
  61. /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`.
  62. ///
  63. /// `src` is expected to be 32 bytes long and encoded using valid characters.
  64. ///
  65. /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
  66. pub fn from_base16(src: &str) -> Result<SpotifyId, SpotifyIdError> {
  67. let mut dst: u128 = 0;
  68. for c in src.as_bytes() {
  69. let p = match c {
  70. b'0'..=b'9' => c - b'0',
  71. b'a'..=b'f' => c - b'a' + 10,
  72. _ => return Err(SpotifyIdError),
  73. } as u128;
  74. dst <<= 4;
  75. dst += p;
  76. }
  77. Ok(SpotifyId::as_track(dst))
  78. }
  79. /// Parses a base62 encoded [Spotify ID] into a `SpotifyId`.
  80. ///
  81. /// `src` is expected to be 22 bytes long and encoded using valid characters.
  82. ///
  83. /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
  84. pub fn from_base62(src: &str) -> Result<SpotifyId, SpotifyIdError> {
  85. let mut dst: u128 = 0;
  86. for c in src.as_bytes() {
  87. let p = match c {
  88. b'0'..=b'9' => c - b'0',
  89. b'a'..=b'z' => c - b'a' + 10,
  90. b'A'..=b'Z' => c - b'A' + 36,
  91. _ => return Err(SpotifyIdError),
  92. } as u128;
  93. dst *= 62;
  94. dst += p;
  95. }
  96. Ok(SpotifyId::as_track(dst))
  97. }
  98. /// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order.
  99. ///
  100. /// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`.
  101. pub fn from_raw(src: &[u8]) -> Result<SpotifyId, SpotifyIdError> {
  102. if src.len() != SpotifyId::SIZE {
  103. return Err(SpotifyIdError);
  104. };
  105. let mut dst = [0u8; SpotifyId::SIZE];
  106. dst.copy_from_slice(src);
  107. Ok(SpotifyId::as_track(u128::from_be_bytes(dst)))
  108. }
  109. /// Parses a [Spotify URI] into a `SpotifyId`.
  110. ///
  111. /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}`
  112. /// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID.
  113. ///
  114. /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
  115. pub fn from_uri(src: &str) -> Result<SpotifyId, SpotifyIdError> {
  116. // We expect the ID to be the last colon-delimited item in the URI.
  117. let b = src.as_bytes();
  118. let id_i = b.len() - SpotifyId::SIZE_BASE62;
  119. if b[id_i - 1] != b':' {
  120. return Err(SpotifyIdError);
  121. }
  122. let mut id = match SpotifyId::from_base62(&src[id_i..]) {
  123. Ok(v) => v,
  124. Err(e) => return Err(e),
  125. };
  126. // Slice offset by 8 as we are skipping the "spotify:" prefix.
  127. id.audio_type = src[8..id_i - 1].into();
  128. Ok(id)
  129. }
  130. /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE62` (22)
  131. /// character long `String`.
  132. pub fn to_base16(&self) -> String {
  133. to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16])
  134. }
  135. /// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22)
  136. /// character long `String`.
  137. ///
  138. /// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
  139. pub fn to_base62(&self) -> String {
  140. let mut dst = [0u8; 22];
  141. let mut i = 0;
  142. let n = self.id;
  143. // The algorithm is based on:
  144. // https://github.com/trezor/trezor-crypto/blob/c316e775a2152db255ace96b6b65ac0f20525ec0/base58.c
  145. //
  146. // We are not using naive division of self.id as it is an u128 and div + mod are software
  147. // emulated at runtime (and unoptimized into mul + shift) on non-128bit platforms,
  148. // making them very expensive.
  149. //
  150. // Trezor's algorithm allows us to stick to arithmetic on native registers making this
  151. // an order of magnitude faster. Additionally, as our sizes are known, instead of
  152. // dealing with the ID on a byte by byte basis, we decompose it into four u32s and
  153. // use 64-bit arithmetic on them for an additional speedup.
  154. for shift in &[96, 64, 32, 0] {
  155. let mut carry = (n >> shift) as u32 as u64;
  156. for b in &mut dst[..i] {
  157. carry += (*b as u64) << 32;
  158. *b = (carry % 62) as u8;
  159. carry /= 62;
  160. }
  161. while carry > 0 {
  162. dst[i] = (carry % 62) as u8;
  163. carry /= 62;
  164. i += 1;
  165. }
  166. }
  167. for b in &mut dst {
  168. *b = BASE62_DIGITS[*b as usize];
  169. }
  170. dst.reverse();
  171. unsafe {
  172. // Safety: We are only dealing with ASCII characters.
  173. String::from_utf8_unchecked(dst.to_vec())
  174. }
  175. }
  176. /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in
  177. /// big-endian order.
  178. pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] {
  179. self.id.to_be_bytes()
  180. }
  181. /// Returns the `SpotifyId` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`,
  182. /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded
  183. /// Spotify ID.
  184. ///
  185. /// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will
  186. /// be encoded as `unknown`.
  187. ///
  188. /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
  189. pub fn to_uri(&self) -> String {
  190. // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
  191. // + unknown size audio_type.
  192. let mut dst = String::with_capacity(31 + self.audio_type.len());
  193. dst.push_str("spotify:");
  194. dst.push_str(self.audio_type.into());
  195. dst.push_str(":");
  196. dst.push_str(&self.to_base62());
  197. dst
  198. }
  199. }
  200. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
  201. pub struct FileId(pub [u8; 20]);
  202. impl FileId {
  203. pub fn to_base16(&self) -> String {
  204. to_base16(&self.0, &mut [0u8; 40])
  205. }
  206. }
  207. impl fmt::Debug for FileId {
  208. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  209. f.debug_tuple("FileId").field(&self.to_base16()).finish()
  210. }
  211. }
  212. impl fmt::Display for FileId {
  213. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  214. f.write_str(&self.to_base16())
  215. }
  216. }
  217. #[inline]
  218. fn to_base16(src: &[u8], buf: &mut [u8]) -> String {
  219. let mut i = 0;
  220. for v in src {
  221. buf[i] = BASE16_DIGITS[(v >> 4) as usize];
  222. buf[i + 1] = BASE16_DIGITS[(v & 0x0f) as usize];
  223. i += 2;
  224. }
  225. unsafe {
  226. // Safety: We are only dealing with ASCII characters.
  227. String::from_utf8_unchecked(buf.to_vec())
  228. }
  229. }
  230. #[cfg(test)]
  231. mod tests {
  232. use super::*;
  233. struct ConversionCase {
  234. id: u128,
  235. kind: SpotifyAudioType,
  236. uri: &'static str,
  237. base16: &'static str,
  238. base62: &'static str,
  239. raw: &'static [u8],
  240. }
  241. static CONV_VALID: [ConversionCase; 4] = [
  242. ConversionCase {
  243. id: 238762092608182713602505436543891614649,
  244. kind: SpotifyAudioType::Track,
  245. uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH",
  246. base16: "b39fe8081e1f4c54be38e8d6f9f12bb9",
  247. base62: "5sWHDYs0csV6RS48xBl0tH",
  248. raw: &[
  249. 179, 159, 232, 8, 30, 31, 76, 84, 190, 56, 232, 214, 249, 241, 43, 185,
  250. ],
  251. },
  252. ConversionCase {
  253. id: 204841891221366092811751085145916697048,
  254. kind: SpotifyAudioType::Track,
  255. uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4",
  256. base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
  257. base62: "4GNcXTGWmnZ3ySrqvol3o4",
  258. raw: &[
  259. 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
  260. ],
  261. },
  262. ConversionCase {
  263. id: 204841891221366092811751085145916697048,
  264. kind: SpotifyAudioType::Podcast,
  265. uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4",
  266. base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
  267. base62: "4GNcXTGWmnZ3ySrqvol3o4",
  268. raw: &[
  269. 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
  270. ],
  271. },
  272. ConversionCase {
  273. id: 204841891221366092811751085145916697048,
  274. kind: SpotifyAudioType::NonPlayable,
  275. uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4",
  276. base16: "9a1b1cfbc6f244569ae0356c77bbe9d8",
  277. base62: "4GNcXTGWmnZ3ySrqvol3o4",
  278. raw: &[
  279. 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216,
  280. ],
  281. },
  282. ];
  283. static CONV_INVALID: [ConversionCase; 2] = [
  284. ConversionCase {
  285. id: 0,
  286. kind: SpotifyAudioType::NonPlayable,
  287. // Invalid ID in the URI.
  288. uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH",
  289. base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9",
  290. base62: "!!!!!Ys0csV6RS48xBl0tH",
  291. raw: &[
  292. // Invalid length.
  293. 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 5, 3, 108, 119, 187, 233, 216, 255,
  294. ],
  295. },
  296. ConversionCase {
  297. id: 0,
  298. kind: SpotifyAudioType::NonPlayable,
  299. // Missing colon between ID and type.
  300. uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH",
  301. base16: "--------------------",
  302. base62: "....................",
  303. raw: &[
  304. // Invalid length.
  305. 154, 27, 28, 251,
  306. ],
  307. },
  308. ];
  309. #[test]
  310. fn from_base62() {
  311. for c in &CONV_VALID {
  312. assert_eq!(SpotifyId::from_base62(c.base62).unwrap().id, c.id);
  313. }
  314. for c in &CONV_INVALID {
  315. assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError));
  316. }
  317. }
  318. #[test]
  319. fn to_base62() {
  320. for c in &CONV_VALID {
  321. let id = SpotifyId {
  322. id: c.id,
  323. audio_type: c.kind,
  324. };
  325. assert_eq!(id.to_base62(), c.base62);
  326. }
  327. }
  328. #[test]
  329. fn from_base16() {
  330. for c in &CONV_VALID {
  331. assert_eq!(SpotifyId::from_base16(c.base16).unwrap().id, c.id);
  332. }
  333. for c in &CONV_INVALID {
  334. assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError));
  335. }
  336. }
  337. #[test]
  338. fn to_base16() {
  339. for c in &CONV_VALID {
  340. let id = SpotifyId {
  341. id: c.id,
  342. audio_type: c.kind,
  343. };
  344. assert_eq!(id.to_base16(), c.base16);
  345. }
  346. }
  347. #[test]
  348. fn from_uri() {
  349. for c in &CONV_VALID {
  350. let actual = SpotifyId::from_uri(c.uri).unwrap();
  351. assert_eq!(actual.id, c.id);
  352. assert_eq!(actual.audio_type, c.kind);
  353. }
  354. for c in &CONV_INVALID {
  355. assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError));
  356. }
  357. }
  358. #[test]
  359. fn to_uri() {
  360. for c in &CONV_VALID {
  361. let id = SpotifyId {
  362. id: c.id,
  363. audio_type: c.kind,
  364. };
  365. assert_eq!(id.to_uri(), c.uri);
  366. }
  367. }
  368. #[test]
  369. fn from_raw() {
  370. for c in &CONV_VALID {
  371. assert_eq!(SpotifyId::from_raw(c.raw).unwrap().id, c.id);
  372. }
  373. for c in &CONV_INVALID {
  374. assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError));
  375. }
  376. }
  377. }