diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2856326..800d3c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.64.0 + toolchain: 1.77.2 override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.64.0 + toolchain: 1.77.2 components: clippy override: true - uses: actions-rs/clippy-check@v1 @@ -68,9 +68,9 @@ jobs: matrix: include: - os: ubuntu-latest - rust: 1.64.0 + rust: 1.77.2 - os: windows-latest - rust: 1.64.0 + rust: 1.77.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@master diff --git a/benches/moar_links.txt b/benches/moar_links.txt new file mode 100644 index 0000000..d5bb877 --- /dev/null +++ b/benches/moar_links.txt @@ -0,0 +1,20 @@ +Let's add some more links just for testing and benching: + +these are some IPv6 links: + +gopher://[::1]/ +https://[::1]/سلام +https://[2345:0425:2CA1:0000:0000:0567:5673:23b5]/hello_world +https://[2345:425:2CA1:0:0:0567:5673:23b5]/hello_world + +an IPvfuture link: +ftp://mrchickenkiller@[vA.A]/var/log/boot.log + +some normal links: + +https://www.ietf.org/rfc/rfc3987.txt +https://iamb.chat/messages/index.html +https://github.com/deltachat/message-parser/issues/67 +https://far.chickenkiller.com +gopher://republic.circumlunar.space +https://far.chickenkiller.com/religion/a-god-who-does-not-care/ diff --git a/benches/my_benchmark.rs b/benches/my_benchmark.rs index 2eb25c7..13bedf5 100644 --- a/benches/my_benchmark.rs +++ b/benches/my_benchmark.rs @@ -1,10 +1,13 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use deltachat_message_parser::parser::{parse_desktop_set, parse_markdown_text, parse_only_text}; +use deltachat_message_parser::parser::{ + parse_desktop_set, parse_markdown_text, parse_only_text, LinkDestination, +}; pub fn criterion_benchmark(c: &mut Criterion) { let testdata = include_str!("testdata.md"); let lorem_ipsum_txt = include_str!("lorem_ipsum.txt"); let r10s_update_message = include_str!("r10s_update_message.txt"); + let links = include_str!("moar_links.txt"); c.bench_function("only_text_lorem_ipsum.txt", |b| { b.iter(|| parse_only_text(black_box(lorem_ipsum_txt))) @@ -35,6 +38,10 @@ pub fn criterion_benchmark(c: &mut Criterion) { c.bench_function("markdown_r10s_update_message.txt", |b| { b.iter(|| parse_markdown_text(black_box(r10s_update_message))) }); + + c.bench_function("parse_link_moar_links.txt", |b| { + b.iter(|| LinkDestination::parse(black_box(links))) + }); } criterion_group!(benches, criterion_benchmark); diff --git a/benches/testdata.md b/benches/testdata.md index ed9b973..eb9773b 100644 --- a/benches/testdata.md +++ b/benches/testdata.md @@ -10,6 +10,766 @@ http://delta.chat?test=1234&y=4 https://delta.chat/hello?test=1234&y=4 + + Then a text containing a delimited link then a [labeled link](https://delta.chat/hello?test=1234&y=4) and a #hashtag, cause why not. `inline code` and more useless text: 1+1 != 3 ; what a user may or may not write in a message somehow. + + +tons of data from awesome bitcoin cash list: + +
+ awesome bitcoin cash +
+
+
+A curated list of Bitcoin Cash projects & resources
+ + awesome + + +
+Bitcoin Cash (BCH) is a project to scale bitcoin on-chain as an electronic peer-to-peer payment system for the world. 🚀 + +
+
+ +📤 [a mobile friendly version](https://awesomebitcoin.cash) of this [project](https://github.com/2qx/awesome-bitcoin-cash) is formatted [from markdown](https://github.com/2qx/awesome-bitcoin-cash/blob/master/README.md) by github pages. + +Pull requests are welcome, please see [the contribution guidelines](CONTRIBUTING.md). +
+ +[![Check Links](https://github.com/2qx/awesome-bitcoin-cash/actions/workflows/links.yml/badge.svg)](https://github.com/2qx/awesome-bitcoin-cash/actions/workflows/links.yml) + + +# Contents + +- [Contents](#contents) +- [Getting Started](#getting-started) +- [State of the Project](#state-of-the-project) +- [Whitepaper](#whitepaper) +- [Open-Source Wallets](#open-source-wallets) + - [Mobile](#mobile) + - [Desktop](#desktop) + - [Electron-Cash Plugins](#electron-cash-plugins) + - [Cli](#cli) + - [Browser](#browser) + - [Paper/Offline Generator](#paperoffline-generator) +- [Podcasts, News, Media](#podcasts-news-media) +- [Projects Built on Bitcoin Cash](#projects-built-on-bitcoin-cash) + - [Apps (Social)](#apps-social) + - [Crowdfunding](#crowdfunding) + - [BCH Native Decentralized Finance](#bch-native-decentralized-finance) + - [Collectables](#collectables) + - [Entertainment](#entertainment) + - [Exchanges](#exchanges) + - [Centralized](#centralized) + - [More decentralized](#more-decentralized) + - [Oracles](#oracles) + - [Faucets](#faucets) + - [Network](#network) + - [Explorers](#explorers) + - [Testnet Explorers](#testnet-explorers) + - [Services](#services) + - [Utilities](#utilities) + - [Web](#web) + - [See Also](#see-also) +- [Merchants and Services Accepting Bitcoin Cash](#merchants-and-services-accepting-bitcoin-cash) + - [A Short List](#a-short-list) + - [Geographic lists](#geographic-lists) + - [Projects dedicated to listing or enabling eCommerce.](#projects-dedicated-to-listing-or-enabling-ecommerce) + - [Some Charities and Foundations](#some-charities-and-foundations) +- [eCommerce Merchant Resources](#ecommerce-merchant-resources) + - [Bitcoin Cash Open-Source plugins](#bitcoin-cash-open-source-plugins) + - [Point of Sale Clients](#point-of-sale-clients) + - [Non-Custodial Payment Processors](#non-custodial-payment-processors) + - [BCH-to-Fiat Payment Processors](#bch-to-fiat-payment-processors) + - [Payment Processor Status](#payment-processor-status) +- [Documentation](#documentation) + - [General](#general) + - [Base Protocol](#base-protocol) + - [Secondary protocols](#secondary-protocols) + - [Discussion](#discussion) + - [CHIP Process](#chip-process) + - [Previous consensus changes, May 2023:](#previous-consensus-changes-may-2023) + - [Bitcoin Script](#bitcoin-script) +- [Software](#software) + - [Full Nodes](#full-nodes) + - [Developer Resources](#developer-resources) + - [Open-Source Teams Building on Bitcoin Cash](#open-source-teams-building-on-bitcoin-cash) + - [Simple Payment Verification (SPV)](#simple-payment-verification-spv) + - [Libraries \& SDKs](#libraries--sdks) + - [Language Agnostic](#language-agnostic) + - [Typescript](#typescript) + - [Javascript](#javascript) + - [Python](#python) + - [Rust](#rust) + - [Java](#java) + - [C](#c) + - [PHP](#php) + - [R](#r) +- [Endorsements](#endorsements) + - [The Adaptive Blocksize Limit Algorithm (ebaa) CHIP for the May 2024 BCH Upgrade is AWESOME!](#the-adaptive-blocksize-limit-algorithm-ebaa-chip-for-the-may-2024-bch-upgrade-is-awesome) + - [The CashTokens and P2SH32 CHIP Proposals for the May 2023 BCH Upgrade are AWESOME!](#the-cashtokens-and-p2sh32-chip-proposals-for-the-may-2023-bch-upgrade-are-awesome) +- [The Archive](#the-archive) + - [Bitcoin Script tools](#bitcoin-script-tools) + - [Simple Ledger Protocol (SLP Token)](#simple-ledger-protocol-slp-token) + - [Protocols](#protocols) + - [Libraries](#libraries) + - [SLP Token Projects](#slp-token-projects) + +# Getting Started + +- [bitcoincash.org](https://bitcoincash.org) - A general multi-lingual introduction. +- [BCH Info](https://bch.info/) - Multilingual site for general information about bitcoin cash. +- [BCHFAQ.com](https://bchfaq.com/) [[code]](https://github.com/fixthetracking/Bitcoin-Cash-FAQ) - Learn the fundamentals of Bitcoin Cash by getting simple answers to your basic questions. +- [Why Bitcoin Cash?](https://whybitcoincash.com/) [[archive]](https://web.archive.org/web/20230228125654/https://whybitcoincash.com/) - The revolution will not be censored. +- [Bitcoin.com Getting Started](https://www.bitcoin.com/get-started/) - Comprehensive introduction for general audiences. +- [Why Cryptocurrencies?](https://whycryptocurrencies.com/toc.html) [[code]](https://github.com/treeman/why_cryptocurrencies) - An explanation on why cryptocurrencies were created, what they do differently and why they matter. + +# State of the Project + +- [Three Years In: A Bitcoin Cash Update From One of Its Founders](https://news.bitcoin.com/three-years-in-a-bitcoin-cash-update-from-one-of-its-founders/) - by Jonald Fyookball + +# Whitepaper + +"Bitcoin: A Peer-to-Peer Electronic Cash System" by Satoshi Nakamoto. + +Bitcoin Cash is one chain of Satoshi Nakamoto's blockchain invention which was deliberately hard-forked on August 1st, 2017. It shares the whitepaper, first block, and all bitcoin block history prior to the fork. It attempts to implement the central idea outlined in that paper. + +Below is a copy of the original nine page whitepaper: + +- [Archived copy](https://web.archive.org/web/20100704213649if_/http://www.bitcoin.org:80/bitcoin.pdf) of the bitcoin whitepaper from bitcoin.org. +- [bitcoin whitepaper](https://gateway.ipfs.io/ipfs/QmRA3NWM82ZGynMbYzAgYTSXCVM14Wx1RZ8fKP42G6gjgj) via ipfs. +- Websites hosting the bitcoin whitepaper [[wayback archive]](http://web.archive.org/web/20210516141704if_/https://blockchair.com/bitcoin/whitepaper), with sha256 hashes calculated as of May 16th 2021. +- [As a webcomic](https://web.archive.org/web/20230215013643/https://whitepaper.coinspice.io/) [[中文]](https://web.archive.org/web/20230315051200/https://whitepaper.coinspice.io/cn) [[日本語]](https://web.archive.org/web/20200217125719/https://www.bitcoin.jp/what-is-bitcoin/bitcoin-whitepaper-comic/) - Bitcoin Whitepaper web comic by Scott McCloud. +- [Instructions and code](https://bitcoin.stackexchange.com/questions/35959/how-is-the-whitepaper-decoded-from-the-blockchain-tx-with-1000x-m-of-n-multisi) for building the original paper encoded on the blockchain on 2013-04-06. + +# Open-Source Wallets + +Below are non-custodial open-source wallets that use features specific to Bitcoin Cash. + +**[Best BCH Wallets](https://www.bestbchwallets.com)** is a tool for selecting a wallet based on operating system and features. + +## Mobile + +- 🔵 [Electron-Cash](https://electroncash.org) - Android [[code]](https://github.com/Electron-Cash/Electron-Cash/tree/master/android) and iOS [[code]](https://github.com/Electron-Cash/Electron-Cash/tree/master/ios) versions available with more limited functionality. +- 🔵 [Paytaca](https://www.paytaca.com/) [[apk]](https://github.com/paytaca/paytaca-app/releases) [[code]](https://github.com/paytaca/paytaca-app) - A mobile wallet for Android, iOS and ChromeOS +- [Flowee Pay](https://flowee.org/products/pay/) [[code]](https://codeberg.org/Flowee/pay/) [[apk]](https://flowee.org/products/pay/) [[docs]](https://codeberg.org/Flowee/Pay/wiki) - A user friendly wallet for Android and Linux desktop. +- [Selene Wallet](https://selene.cash/) [[code]](https://git.xulu.tech/selene.cash/selene-wallet/) - Easy, no-hassle, instant payments in the palm of your hand. +- [Stack Wallet](https://stackwallet.com/) [[code]](https://github.com/cypherstack/stack_wallet) - Multicoin wallet with UTXO (coin) control. +- [Cake Wallet](https://cakewallet.com/) [[code]](https://github.com/cake-tech/cake_wallet) [[apk]](https://github.com/cake-tech/cake_wallet/releases) - An open source wallet for iOS and Android supporting XMR and other currencies. +- 🔵 [zapit](https://zapit.io/#/)* - A native, non-custodial Bitcoin Cash wallet for iOS and Android. *Not open source + +## Desktop +- 🔵 [Electron Cash CashToken](https://electroncash.org) [[release]](https://github.com/Electron-Cash/Electron-Cash/releases/tag/4.3.0) [[code]](https://github.com/Electron-Cash/Electron-Cash/) - Electron Cash with CashTokens. +- [Flowee Pay](https://flowee.org/products/pay/) [[code]](https://codeberg.org/flowee/pay) - A payment solution, a wallet, a basis for your new product. But currently just a desktop wallet. +- 🔵 [Cashonize (quasar)](https://github.com/cashonize/cashonize-quasar/releases/tag/v0.0.2) [[code]](https://github.com/cashonize/cashonize-quasar) - Cashonize rewrite with Quasar & Vue-js + +### Electron-Cash Plugins + +- [Flipstarter Plugin](https://gitlab.com/flipstarter/flipstarter-electron-cash) - plugin for crowdfunding. +- [Nostron](https://github.com/Electron-Cash/Nostron/) - Nostron is a plugin for the Electron-Cash BCH wallet. +- [Inter-Wallet Transfer plugin](https://github.com/KarolTrzeszczkowski/Inter-Wallet-Transfer-EC-plugin) - A plugin, that sends your coins to another wallet one by one, every time to a fresh address. +- [Mecenas Plugin](https://github.com/KarolTrzeszczkowski/Mecenas-recurring-payment-EC-plugin/releases) - recurring payments. +- [Last Will](https://github.com/KarolTrzeszczkowski/Electron-Cash-Last-Will-Plugin) - dead man smart contract creation. +- [HODL](https://github.com/mainnet-pat/hodl_ec_plugin/) - smart contract plugin for Electron Cash to timelock funds. +- [AutoCove](https://github.com/TinosNitso/AutoCove-Plugin) - Electrum-cash script decoder. + +## Cli + +- [bitcore-wallet](https://github.com/bitpay/bitcore/tree/master/packages/bitcore-wallet) - A command line wallet used for BitPay wallets. + +## Browser +- 🔵 [Cashonize](https://cashonize.com/) [[code]](https://github.com/cashonize/wallet) - An experimental web wallet for CashTokens. +- [PSF wallet](https://wallet.fullstack.cash/) [[code]](https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet) - An web wallet with SLP support. +- 🔵 [Microfi Wallet](https://microfi.eu/wallet/) - Microfi Free Flow Wallet +- [BCH Merchant PoS](https://pos.cash) [[code]](https://github.com/softwareverde/pos-cash) - Bitcoin Cash Web Point of Sale, from SoftwareVerde. + +## Paper/Offline Generator + +- [Cash Address Generator](https://cashaddress.org/) [[code]](https://github.com/theantnest/bccaddress) - reputable javascript address generator suitable for offline use. +- [Bitcoin.com Paper Wallet](https://paperwallet.bitcoin.com/) [[code]](https://github.com/Bitcoin-com/paperwallet.bitcoin.com) - A fork of the cashaddress.org paper wallet +- Keep Bitcoin Free Paper Wallet [[code]](https://github.com/KeepBitcoinFree-org/paper.keepbitcoinfree.org) - A fork of the Bitcoin.com paper wallet +- [BCH Gifts](https://gifts.bitcoin.com/) - generate reclaimable preloaded paper private keys as gifts. + +# Podcasts, News, Media + +Bitcoin Cash focussed media and content. + +- [The Bitcoin Cash Podcast](https://www.bitcoincashpodcast.com) - Available on [Youtube](https://www.youtube.com/channel/UCsrDsJnHFnkMnJhEslofyPQ) and [RSS](https://rss.com/podcasts/bitcoincashpodcast/) audio versions, plus other video and podcast platforms (see links at bottom of website). +- [Bitcoin Cash Foundation](https://bitcoincashfoundation.org/) Weekly News - Available on [Youtube](https://www.youtube.com/@BitcoinCashFoundation) and [Telegram](https://t.me/BCHFNews) +- General Protocol Spaces - Available on [Youtube](https://www.youtube.com/watch?v=707-DPzhdA8&list=PLcIK2erO9hWyM56FYkUAilfUmABbwpu7U) and twitter. + + +# Projects Built on Bitcoin Cash + +All of these apps are mostly stable and active. Always check the notes of a particular project before risking a large sum of value. Links are checked on a weekly basis, but function is not checked. + +## Apps (Social) + +- [read.cash](https://read.cash) - a conventionally hosted long-format blogging platform, with BCH tipping for content. +- [memo.cash](https://memo.cash) - short message social media site with decentralized SLP token exchange. +- [Cashrain](https://cashrain.com/) - A platform where creators create communities for their members. +- [noise.app](https://noise.app) - An invite only Bitcoin Cash powered micro-blogging platform. +- [OnlyCoins](https://onlycoins.com/) - Adult content monetization platform. +- [Glimpse.cash](https://glimpse.cash/) - A pay per view video hosting and streaming platform. +- [Gaze.cash](https://gaze.cash/) - A more lenient pay-per-view video platform. +- [WhoTipped.it](https://whotipped.it/) - Last tips given on memo.cash + +## Crowdfunding + +- [flipstarter](https://flipstarter.cash/) [[Introduction]](https://read.cash/@flipstarter/introducing-flipstarter-695d4d50) [[code]](https://gitlab.com/flipstarter/backend) - a crowd funding app using anyone can pay multisig transactions. +- IPFS Flipstarter [[code]](https://gitlab.com/ipfs-flipstarter) - An IPFS flipstarter campaign site. + +## BCH Native Decentralized Finance + +[DefiLlama](https://defillama.com/chain/Bitcoincash) - Statistics for Bitcoin Cash Defi. + +- [BCH Bull](https://bchbull.com/) [[app]](https://app.bchbull.com/) - Permissionless leverage and hedging using the Anyhedge protocol. +- 🔵 [TapSwap](https://tapswap.cash/) - An open marketplace for fungible and non-fungible tokens. +- 🔵 [Cauldron](https://www.cauldron.quest/) [[whitepaper]](https://www.cauldron.quest/_files/ugd/ae85be_b1dc04d2b6b94ab5a200e3d8cd197aa3.pdf) - A Constant product market maker contract +- [Unspent](https://unspent.cash) [[code]](https://github.com/2qx/unspent) [[cli]](https://www.npmjs.com/package/unspent) [[docs]](https://unspent.app/documentation) - An irrevocable perpetuity app +- 🔵 [Emerald DAO](https://emerald-dao.cash/) [[app]](https://emerald-dao.vercel.app/) [[code]](https://gitlab.com/0353F40E/emerald-dao/) - A simple Bitcoin Cash DAO template which acts as a fixed-term deposit savings vault. +- 🔵 [Wrapped Cash](https://wrapped.cash/) [[code]](https://gitlab.com/dagurval/wrapped-cash) - Bitcoin Cash wrapped as a CashToken + + + +## Collectables + +- 🔵 [BCH Guru NFTs](https://nfts.bch.guru) - a premier collection of NFTs +- 🔵 [Ghostwriter](https://ghostwriter.pages.dev/) - Text based NFT minting +- 🔵 [Bitcats Heroes](https://bitcatsheroes.club/) - Collectibele NFT series with non-custodial minting contract. +- 🔵 [CashNinjas](https://ninjas.cash/) [[code]](https://github.com/cashninjas) - an NFT collection leveraging the new CashTokens technology. + + +## Entertainment + +- [bch.games](https://bch.games/) - dice and numbers game. +- 🔵 [BCU Guru](https://bch.guru) - A peer to peer price prediction game on Bitcoin Cash +- 🔵 [DogeCash](https://dogecash.uwu.ai/) - Don't let your dreams be memes +- [craft.cash](https://craft.cash/) [[code]](https://github.com/blockparty-sh/craft.cash) - Voxel world stored on Bitcoin Cash. +- [Satoshi dice](https://www.satoshidice.com/) - a provably fair dice game. +- [Spin BCH](https://SpinBCH.com) - Spinning wheel based gambling using zero-conf + +## Exchanges + +Bitcoin Cash is supported on hundreds of exchanges, these are a few. + +### Centralized + +- [CoinEx](https://www.coinex.com/) - A BCH friendly exchange with automatic coin-splitting + +### More decentralized + +- [Thorchain Swap](https://app.thorswap.finance/) - Swap native assets directly with any non-custodial wallet across nine blockchains. +- [Komodo Wallet](https://app.komodoplatform.com/) - Decentralized exchange with desktop clients supporting BCH and many UTXO coins, ETH, ERC-20 tokens + +## Oracles + +- [Oracles.Cash](https://oracles.cash/) [[Best Practices]](https://gitlab.com/GeneralProtocols/priceoracle/library#best-practices-for-price-oracle-consumers) [[spec]](https://gitlab.com/GeneralProtocols/priceoracle/specification) - Price oracles for Bitcoin Cash + +## Faucets + +- 🔵 [Testnet Faucet](https://tbch.googol.cash/) [[code]](https://gitlab.com/uak/light-crypto-faucet) +- 🔵 [`unspent`](https://www.npmjs.com/package/unspent?activeTab=readme) [[code]](https://github.com/2qx/unspent) - an javascript package with commands for faucets. +- BCH Testnet Faucet [[code]](https://github.com/christroutner/testnet-faucet2/) - Fullstack.cash faucet for tBCH. + +## Network + +- [fork.lol](https://fork.lol) - Site to monitor network health in relation to BTC. +- [Johoe's Bitcoin Mempool Statistics](https://jochen-hoenicke.de/queue/) [[code]](https://github.com/jhoenicke/mempool) - Colorful mempool graphs. +- [Electrum Server Status for BCH](https://1209k.com/bitcoin-eye/ele.php?chain=bch) [[or tBCH]](https://1209k.com/bitcoin-eye/ele.php?chain=tbch) - A 1209k hosted list of electrum servers +- [Tx Street](https://txcity.io/v/bch-eth) [[code]](https://github.com/txstreet/txstreet) - a live blockchain transaction and mempool visualizer. +- [Bitcoin Energy Statistics](https://www.monsterbitar.se/~jonathan/energy/) - A comparison of energy usage for BCH and BTC. + +### Explorers +- 🔵 [Blockchain Explorer](https://explorer.bch.ninja/) [[code]](https://github.com/sickpig/bch-rpc-explorer) [[mirror: BU]](https://explorer.bitcoinunlimited.info/) [[mirror: electroncash.de]](https://explorer.electroncash.de) - Database-free, self-hosted Bitcoin Cash explorer, via RPC. +- 🔵 [Bitcoin Cash Explorer](https://explorer.salemkode.com/) [[code]](https://github.com/salemkode/explorer) - A Bitcoin Cash Explorer with CashTokens, by SalemKode. +- 🔵 [3xpl.com BCH Explorer](https://3xpl.com/bitcoin-cash) [[code]](https://github.com/3xplcom)- Fastest ad-free universal block explorer. +- [BCH Explorer](https://explorer.melroy.org/) [[code]](https://gitlab.melroy.org/bitcoincash/explorer) - Bitcoin Cash Explorer by Melroy van den Berg +- [Blockchair BCH Explorer](https://blockchair.com/bitcoin-cash) - Universal blockchain explorer and search engine. +- [Blockchain.com BCH explorer](https://www.blockchain.com/explorer?view=bch) - Established blockchain explorer. +- 🔵 [BCH CashTokens NFT Viewer](https://viewer.sploit.cash) [[code]](https://github.com/acidsploit/cashtokens-nft-viewer) - Sploit's NFT viewer. + ### Testnet Explorers + - 🔵 [Chipnet (im_uname)](https://chipnet.imaginary.cash) + - 🔵 [Chipnet (chaingraph)](https://chipnet.chaingraph.cash) + - 🔵 [Chipnet (bch.ninja)](https://chipnet.bch.ninja) + - [Testnet [old]](https://texplorer.bitcoinunlimited.info/), [[mirror]](https://testnet-explorer.electroncash.de/) +- [Chaingraph](https://chaingraph.cash/) [[code]](https://github.com/bitauth/chaingraph) - A multi-node blockchain indexer and GraphQL API. +- [CoinGecko API](https://www.coingecko.com/api/documentation) - Free tier api for price data. +- [Blockchair Bulk Data](https://gz.blockchair.com/bitcoin-cash/) - Daily compressed dumps of blockchain data. +- [CashFusion Stats](https://fusionstats.redteam.cash/) - Data on privacy-enhancing CashFusion transactions. +- [Mempool Project](https://bchmempool.cash/) - A Bitcoin Cash (BCH) adaptation of the mempool open-source explorer. +- [bitcoinfees.cash](https://bitcoinfees.cash/) - bitcoin chain fee juxtaposition. + +## Services + +- 🔵 [OpenTokenRegistry](https://otr.cash/) [[code]](https://github.com/OpenTokenRegistry/otr.cash) - Community-Verified Token Information +- 🔵 [IPFS-BCH](https://ipfs-bch.pat.mn/) [[code]](https://github.com/mainnet-pat/ipfs-bch.pat.mn) - IPFS file pinning service with on-chain settlement +- [CashTags](https://tags.infra.cash/) [[code]](https://github.com/developers-cash/cashtags-server) - Service for printable QR Codes (Payment URLs) whose value amounts can be specified in fiat (e.g. USD). +- [SideShift.ai](https://sideshift.ai/) - enables HUMANS and AI to shift between 30+ cryptocurrencies. +- 🔵 [Token Stork](https://tokenstork.com/) - A CashToken market capitalization explorer. +- 🔵 [Token Explorer](https://tokenexplorer.cash/) - A Token explorer for CashTokens. +- [Chaintip Bounties](https://github.com/chaintip/bounties/blob/master/README.md#available-bounties) - BCH bot for github bounties. +- [BCH.gg](https://bch.gg/) - Bitcoin Cash URL Shortener + +## Utilities + +- [CashAccount](https://www.cashaccount.info/) - Online utility for cashaccounts (address handles). +- 🔵 [Bitauth IDE](https://ide.bitauth.com/) [[code]](https://github.com/bitauth/bitauth-ide) [[walk-thru]](https://www.youtube.com/watch?v=o-igo-adS8E) - An online IDE for developing Bitcoin Cash contracts. +- 🔵 [CashTokens Studio](https://cashtokens.studio/) - CashToken and Authkey creation tool ([chipnet](https://chipnet.cashtokens.studio/)) +- [Bitcoin.com Tools](https://tools.bitcoin.com/) - A mix of Bitcoin utilities. +- 🔵 [CashTokens Airdrop Tool](https://github.com/mr-zwets/airdrop-tool) - A command line utility to airdrop fungible tokens to NFT holders. + +## Web + +- [Bitcoin Paywall](https://wordpress.org/plugins/bitcoin-paywall/) [[code]](https://plugins.trac.wordpress.org/browser/bitcoin-paywall/) - Wordpress paywall plugin + +## See Also + +These are other projects dedicated to listing projects in the Bitcoin Cash ecosystem: + +- [HelpMe Cash](https://helpme.cash/) - A collection of links to things related to the cryptocurrency Bitcoin Cash +- [Bitcoin Cash Projects](https://www.bitcoin.com/bitcoin-cash-projects/) - maintained by bitcoin.com. +- [BCH Developments](https://keepbitcoinfree.org/bch-dev/) - list maintained by KeepBitcoinFree. +- [Canonical awesome-bitcoin-cash](https://github.com/dsmurrell/awesome-bitcoin-cash) - the original. +- [Mainnet Cash List](https://mainnet.cash/projects.html) - A list of projects maintained at mainnet.cash +- [BCHGANG Link Directory](https://bchgang.org) - A directory of links about the cryptocurrency Bitcoin Cash: wallets, merchants, exchanges, tools, references, block explorer, developer guides, tutorials and more. + +# Merchants and Services Accepting Bitcoin Cash + +## A Short List + +These vendors have accepted bitcoin for years and are committed (or sympathetic) toward the idea of electronic cash payments. + +Although some of these may appear to only accept Bitcoin (BTC), they do, in fact, accept Bitcoin Cash also. + +- [Namecheap](https://namecheap.com) - dns, ssl and some packaged hosting. +- [keys4coins](https://www.keys4coins.com/) - Buy PC games and gift cards with cryptocurrency. +- [alfa.top](https://alfa.top/) - Buy mobile top-up (credit) and internet with cryptocurrency. +- [CheapAir](https://www.cheapair.com) - for your travel needs. +- [Travala](https://www.travala.com) - for your travel needs. +- [items sold by Newegg](https://kb.newegg.com/knowledge-base/using-bitcoin-on-newegg/) - good for a great headset. + +## Geographic lists + +- [OpenStreetMap BCH Tag](https://overpass-turbo.eu/?w=%22currency%3ABCH%22%3D%22yes%22+global&R) - Entries tagged with `currency:BCH=yes` in OSM. +- [Bitcoin.com map](https://map.bitcoin.com/) - website and mobile app for discovering merchants, formerly marco coino. +- [Bmap.app](https://bmap.app/) - ₿itcoin places all around the world! +- [where2cash](https://where2.cash/) - Bitcoin Cash Map using OpenStreeMap data. +- [map.usecash](https://map.usecash.com)[[code]](https://github.com/modenero/use-cash) - Use Cash map built by Modenero. + +## Projects dedicated to listing or enabling eCommerce. + +- [Use.Cash](https://usecash.com/) - Guide for using cryptocurrency like cash. +- [Bitgree](https://www.bitgree.com) - service to privately purchase goods on Amazon.com and others at a discount. + +## Some Charities and Foundations + +Just some good charities for the world at large. + +- [Tails](https://tails.boum.org/donate/index.en.html) - The Amnesic Incognito Live System, is a security-focused Debian-based Linux distribution aimed at preserving privacy and anonymity. +- [Save the Children](https://files.savethechildren.org/cryptocurrency-donation/) - **A United Kingdom based charity, founded in 1919**, to improve the lives of children through better education, health care, and economic opportunities, as well as providing emergency aid in natural disasters, war, and other conflicts. (Cryptocurrency donations are powered by [The Giving Block](https://www.thegivingblock.com/)) +- [The Internet Archive](https://blockchair.com/bitcoin-cash/address/1Archive1n2C579dMsAu3iC6tWzuQJz8dN) - 1Archive1n2C579dMsAu3iC6tWzuQJz8dN +- [Bitpay Charity Directory](https://bitpay.com/directory/nonprofits) A list of charities that accept Bitcoin Cash and other cryptocurrencies. + +# eCommerce Merchant Resources + +## Bitcoin Cash Open-Source plugins + +- [CryptoWoo for WooCommerce](https://github.com/WeProgramIT/cryptowoo-bitcoin-cash-addon) - Bitcoin Cash integration for CryptoWoo + +## Point of Sale Clients + +- 🔵 [Paytaca](https://www.paytaca.com/) [[apk]](https://github.com/paytaca/paytaca-app/releases) [[code]](https://github.com/paytaca/paytaca-app) - A mobile wallet with integrated POS. +- [pos.cash](https://pos.cash) [[code]](https://github.com/softwareverde/pos-cash) - a non-custodial web-based point of sale BCH client. + +## Non-Custodial Payment Processors + +- [Prompt.cash](https://prompt.cash) [[demo]](https://www.youtube.com/watch?v=8TIpZW1P_9M) [[docs]](https://prompt.cash/pub/docs/#introduction) - a non-custodial Bitcoin Cash payment gateway +- [Cash Pay Server](https://github.com/developers-cash/cash-pay-server-js) [[docs]](https://developers-cash.github.io/cash-pay-server-js/) - a self-hostable NodeJS micro-service that can be used to handle BIP70 and JSON Payment Protocol invoices for Bitcoin Cash (BCH) + +## BCH-to-Fiat Payment Processors + +- [BitPay developer Integrations](https://bitpay.com/integrations/) [[api docs]](https://bitpay.com/docs) + +## Payment Processor Status + +- [status.bitpay.com](https://status.bitpay.com/) - Current status with recent incidents. + +# Documentation + +## General + +- [developers.cash](https://developers.cash/) - many useful resources +- [Permissionless Software Foundation Videos](https://psfoundation.cash/video/) +- [Electron Cash Wiki](https://wiki.electroncash.de/wiki/Main_Page) + +## Base Protocol + +- [BCH Specification](https://flowee.org/docs/spec/) - Specification hosted by flowee.org. +- [Bitcoin Cash Protocol Documentation](https://documentation.cash/) [[code]](https://github.com/SoftwareVerde/bitcoin-cash-specification) - maintained by Software Verde. +- [reference.cash](https://reference.cash) - protocol documentation +- [Upgrade specs](https://upgradespecs.bitcoincashnode.org/) - Bitcoin Cash upgrade specifications as implemented by BCHN. + +### Secondary protocols + +[Bitcoin Cash Standards](https://bitcoincashstandards.org) is a site dedicated to collecting, some of which are listed below: + +- [AnyHedge](https://anyhedge.com/) [[docs]](https://anyhedge.com/developers/) [[code]](https://gitlab.com/GeneralProtocols/anyhedge) - Decentralized hedge solution against arbitrary commodities for Bitcoin Cash +- 🔵 [Bitcoin Cash Metadata Registries (BCMR)](https://cashtokens.org/docs/bcmr/chip/) [[code]](https://github.com/bitjson/chip-bcmr) - A standard for sharing authenticated metadata between Bitcoin Cash wallets. +- [Cashaddr](https://upgradespecs.bitcoincashnode.org/cashaddr/) - Format for Bitcoin Cash addresses. +- [Cash Accounts](https://gitlab.com/cash-accounts/specification/blob/master/SPECIFICATION.md) - attach a human readable name to Bitcoin Cash addresses. +- CashFusion(https://cashfusion.org) [[spec]](https://github.com/cashshuffle/spec/blob/master/CASHFUSION.md) - a privacy protocol for privately and trustlessly joining coin amounts. +- [CashID](https://gitlab.com/cashid/protocol-specification) - Specification using Bitcoin Cash for secure authentication. +- 🔵 [CashTokens](https://cashtokens.org/) [[code]](https://github.com/cashtokens/cashtokens.org) - Specification for CashTokens. +- [Electrum Cash Protocol (Fulcrum)](https://electrum-cash-protocol.readthedocs.io/en/latest/) [[code]](https://github.com/cculianu/electrum-cash-protocol) - ElectrumX Protocol for [fulcrum](https://fulcrumserver.org) (UTXO indexer/SPV service). +- [Electrum Cash Protocol](https://bitcoincash.network/electrum/) [[code]](https://github.com/dagurval/electrum-cash-protocol) - Protocol for SPV clients and servers. +- [Payment Requests Specification (BIP-0070)](https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki) - For dealing with invoice style payments at specific amounts. +- [Price Oracle](https://gitlab.com/GeneralProtocols/priceoracle/specification) [[implementation]](https://gitlab.com/GeneralProtocols/priceoracle/library) - Price oracle. +- [Memo Protocol](https://memo.cash/protocol) - for the on-chain tweet style social media app. +- [CashShuffle](https://cashshuffle.com/) [[spec]](https://github.com/cashshuffle/spec/blob/master/SPECIFICATION.md) - a privacy protocol for combining transactions with others, splitting to the lowest common amount. + +## Discussion + +An archive of past and future ideas for Bitcoin Cash ongoing at Bitcoin Cash Research (BCR). Collaborating participants have recorded their thoughts and concerns about various potential ideas & implemented improvements. + +- [Bitcoin Cash Research](https://bitcoincashresearch.org/) - Site dedicated to technical discussion. + +## CHIP Process + +Protocol changes, software standards and application specifications may be proposed by anyone. The recommended process for consensus building and conflict reduction is known as the Cash Improvement Proposal (CHIP) Process. + +- [CHIP Guidelines](https://gitlab.com/ggriffith/cash-improvement-proposals/-/blob/master/CHIP-2020-11-CHIP-Guidelines.md) +- [CHIPs: A more detailed process recommendation](https://gitlab.com/im_uname/cash-improvement-proposals/-/blob/master/CHIPs.md) +- [CHIPs](https://bitcoincashresearch.org/c/chips/) - a dynamic list of proposed standards +- [List of CHIPs](https://bch.info/chips) - documents that record proposals to upgrade the Bitcoin Cash protocol, and their ongoing progress, both technical and consensus-building. + +### Previous consensus changes, May 2023: + +- [CHIP-2021-01 Restrict Transaction Version (v1.0)](https://gitlab.com/bitcoin.cash/chips/-/blob/master/CHIP-2021-01-Restrict%20Transaction%20Versions.md) +- [CHIP-2021-01 Minimum Transaction Size (v0.4)](https://gitlab.com/bitcoin.cash/chips/-/blob/master/CHIP-2021-01-Allow%20Smaller%20Transactions.md) +- [CHIP-2022-02 CashTokens (v2.2.1)](https://github.com/bitjson/cashtokens/) +- [CHIP-2022-05 P2SH32 (v1.5.1)](https://gitlab.com/0353F40E/p2sh32/-/blob/main/CHIP-2022-05_Pay-to-Script-Hash-32_(P2SH32)_for_Bitcoin_Cash.md) + +Anyone may propose an improvement to Bitcoin Cash, but the responsibility is on the CHIP owner to see the idea through to fruition and build consensus. + +## Bitcoin Script + +- 🔵 [Cashscript](https://cashscript.org/docs/basics/about/) [[code]](https://github.com/Bitcoin-com/cashscript) [[playground]](https://playground.cashscript.org/) - a solidity-style language that compiles to Bitcoin Cash Script. +- 🔵 [bitauth ide](https://ide.bitauth.com/) [[code]](https://github.com/bitauth/bitauth-ide) [[video intro]](https://www.youtube.com/watch?v=o-igo-adS8E) - an integrated development environment for bitcoin authentication. +- [AutoCove](https://github.com/TinosNitso/AutoCove-Plugin) - Electrum-cash script decoder. +- [Cashscript VSCode plugin](https://marketplace.visualstudio.com/items?itemName=nathanielcherian.cashscript) [[code]](https://github.com/nathanielCherian/vscode-cashscript) - Visual Studio Code extension for cashscript. + +# Software + +## Full Nodes + +- 🔵 [BCHN](https://bitcoincashnode.org/) [[code]](https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node) [[docs]](https://docs.bitcoincashnode.org/) - a descendant of the Bitcoin Core and Bitcoin ABC software projects with independent development team. C/C++. +- 🔵 [BitcoinUnlimited](https://www.bitcoinunlimited.info/) [[code]](https://github.com/BitcoinUnlimited/BitcoinUnlimited) - a full node implentation focused on supporting user needs, C/C++. + - [Bitcoin Unlimited Improvement Proposals (BUIPS)](https://www.bitcoinunlimited.info/voting/) +- 🔵 [Flowee the Hub](https://flowee.org/) [[code]](https://codeberg.org/Flowee/thehub) - a node supporting a suite of software focused on payment integration. C++ +- 🔵 [Bitcoin Verde](https://bitcoinverde.org/) [[code]](https://github.com/softwareverde/bitcoin-verde) [[docs]](https://explorer.bitcoinverde.org/documentation/) - java implementation with the goal of being interoperable with mining nodes. +- 🔵 [Knuth](https://kth.cash/) [[code]](https://github.com/k-nuth/kth) - a high performance implementation of the Bitcoin protocol focused on applications needing extra capacity and resilience. +- [bchd](https://bchd.cash/) [[code]](https://github.com/gcash/bchd) [[docs]](https://github.com/gcash/bchd/tree/master/docs) - [DEPRECATED] alternative implementation written in Go (golang) + +### Developer Resources + +- [Bitcoin Cash Research](https://bitcoincashresearch.org/) - Site dedicated to technical research on Bitcoin Cash. + +## Open-Source Teams Building on Bitcoin Cash + +> If you want to go fast, go alone. If you want to go far, go together. +> +> -- An African Proverb. + +There are various groups developing software stacks & apps for the broader ecosystem. + +- [General Protocols](https://GeneralProtocols.com) [[repos]](https://gitlab.com/GeneralProtocols) - Team researching and developing protocols for non-custodial and trustless networks using BitBox. (Typescript and Javascript) +- [Electron Cash](https://electroncash.org/) [[repos]](https://github.com/Electron-Cash/) - Team maintaining a desktop SPV wallet with plugins and mobile app (Python) +- [Flowee](https://flowee.org) [[repos]](https://codeberg.org/Flowee) - Team maintaining a non-mining full node and services to access the Bitcoin Cash network. (C++, NodeJs et al) +- [FullStack Cash](https://fullstack.cash/) [[repos]](https://github.com/Permissionless-Software-Foundation) - Team building web/ipfs apps based on BitBox compatible stack. (Javascript) +- [Mainnet Cash](https://mainnet.cash/) [[repos]](https://github.com/mainnet-cash/) - Loose-knit team maintaining a shared server-side and client-side library. + +## Simple Payment Verification (SPV) + +- 🔵 [Fulcrum](https://fulcrumserver.org) [[repos]](https://github.com/cculianu/Fulcrum/) - A fast & nimble SPV Server for Bitcoin Cash. +- 🔵 [Rostrum](https://gitlab.com/bitcoinunlimited/rostrum) - Rostrum is an efficient implementation of Electrum Server written in Rust. + +## Libraries & SDKs + +- [Developer tools](https://bch.info/en/developers) - Page devoted to high level developer tools. +- [Mainnet Cash List](https://mainnet.cash/for-developers.html) - A list of useful services for developers. + +### Language Agnostic + +- 🔵 [mainnet](https://mainnet.cash/) [[tutorial]](https://mainnet.cash/tutorial/) [[rest spec]](https://rest-unstable.mainnet.cash/api-docs/#/) - Typescript library, also available via rest api, or [python](https://github.com/mainnet-cash/mainnet-python-generated), [golang](https://github.com/mainnet-cash/mainnet-go-generated), [php](https://github.com/mainnet-cash/mainnet-php-generated) clients, [et. al](https://mainnet.cash/tutorial/other-languages.html) +- [Insomnia](https://insomnia.fountainhead.cash/) [[code]](https://github.com/fountainhead-cash/insomnia) - Swagger/OpenAPI3 specification for ElectrumX +- [BitBox OpenAPI 3 (Swagger) spec](https://github.com/Bitcoin-com/rest.bitcoin.com/tree/master/swaggerJSONFiles) - for rest.bitcoin.com see: [openapi-generator](https://github.com/OpenAPITools/openapi-generator) + +### Typescript + +- 🔵 [Libauth](https://libauth.org/) [[code]](https://github.com/bitauth/libauth) - an ultra-lightweight, zero-dependency library for Bitcoin Cash and Bitauth applications. (Formerly `bitcoin-ts`.) +- 🔵 [electrum-cash](https://gitlab.com/electrum-cash) [[docs]](https://electrum-cash.gitlab.io/network/) [[tutorials]](https://read.cash/search?q=electrum-cash) - JavaScript library that lets you connect with one or more Electrum servers. +- [flowee-js](https://flowee.org/floweejs/) [[docs]](https://flowee.org/docs/) [[code]](https://codeberg.org/Flowee/js) - Bindings for using Flowee applications and libraries with the NodeJS JavaScript engine. +- 🔵 [mainnet-js](https://mainnet.cash/) [[code]](https://github.com/mainnet-cash/mainnet-js) - Typescript library, also available over rest. +- [``](https://github.com/bitjson/qr-code) [[demo]](https://qr.bitjson.com/) – A no-framework, no-dependencies, customizable, animate-able, SVG-based `` HTML element. + +### Javascript + +- [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) [[docs]](https://bchjs.fullstack.cash/) - JavaScript library for creating web and mobile apps that can interact with the Bitcoin Cash (BCH) and eCash (XEC) blockchains +- [electrum-cli](https://github.com/rkalis/electrum-cli) - Super simple command line electrum client. +- [bitcore-lib-cash](https://github.com/bitpay/bitcore/tree/master/packages/bitcore-lib-cash) - javaScript library, maintained by bitpay. + +### Python + +- 🔵 [bitcash](https://pybitcash.github.io/bitcash/) [[code]](https://github.com/pybitcash/bitcash) [[docs]](https://bitcash.dev) - python3 library. +- [jtoomim/p2pool](https://github.com/jtoomim/p2pool) - jtoomim fork of bitcoin pool mining software. + +### Rust + +- 🔵 [rust-bitcoincash](https://gitlab.com/rust-bitcoincash/rust-bitcoincash/) - Rust Bitcoin Cash library. + +### Java + +- [bitcoincashj](https://github.com/pokkst/bitcoincashj) - Bitcoin Cash library for Java + +### C + +- [Breadwallet Core](https://github.com/breadwallet/breadwallet-core) - SPV bitcoin C library. + +### PHP + +- [cashp](https://github.com/Ekliptor/cashp) - Library for BCH. + +### R + +- [rbch](https://cran.r-project.org/package=rbch) - Extraction and Statistical Analysis of Data from the BCH Blockchain + +# Endorsements + +Below is a list of endorsements made in the [Chip Process](#chip-process) in reverse chronological order. + +## The [Adaptive Blocksize Limit Algorithm (ebaa) CHIP](https://gitlab.com/0353F40E/ebaa) for the May 2024 BCH Upgrade is AWESOME! + +[a42f44791b343ffcc118b0dd6645972e9a165e83](https://gitlab.com/0353F40E/ebaa/-/commit/a42f44791b343ffcc118b0dd6645972e9a165e83) + + +## The [CashTokens](https://bitcoincashresearch.org/t/chip-2022-02-cashtokens-token-primitives-for-bitcoin-cash/725) and [P2SH32 CHIP](https://bitcoincashresearch.org/t/chip-2022-05-pay-to-script-hash-32-p2sh32-for-bitcoin-cash/806) Proposals for the May 2023 BCH Upgrade are AWESOME! + +[539b2a492002da881a9ef9aa6604327299c7a498](https://github.com/bitjson/cashtokens/commit/539b2a492002da881a9ef9aa6604327299c7a498) + + + +# The Archive + +Due to the nature of bitcoin, some stuff is forever... + +- [chaintip](https://www.chaintip.org) - An on-chain non-custodial tipping bot for reddit/twitter & github. [DEPRECATED due to reddit API access changes] + +## Bitcoin Script tools + +- [spedn](https://spedn.pl/) [[code]](https://bitbucket.org/o-studio/spedn/src/develop/) [[docs]](https://spedn.readthedocs.io/en/latest/) - a high level smart contract language that compiles to Bitcoin Cash Script. +- [meep](https://github.com/gcash/meep) - a command line Bitcoin Cash script debugger. + +## Simple Ledger Protocol (SLP Token) + +The Permissionless Software Foundation is actively maintaining an SLP wallet and indexer, denoted with starts (⭐) below. + +### Protocols + +- Simple Ledger Protocol (SLP) [[specs]](https://slp.dev) - for handling ERC-20 style tokens. +- [Simple Ledger Postage Protocol](https://github.com/simpleledger/slp-specifications/blob/master/slp-postage-protocol.md) - Protocol for sending SLP tokens without BCH "gas". + +### Libraries + +- **⭐ SLP Indexer ⭐** [[code]](https://github.com/Permissionless-Software-Foundation/psf-slp-indexer) - Functional SLP token indexer running token infrastructure for several businesses. +- Simple Ledger [[repos]](https://github.com/simpleledger) - Group leading SLP token integration. (Typescript & Python) +- [SLP Explorer](https://simpleledger.info/) [[code]](https://github.com/salemkode/slp-explorer) [[backend src]](https://github.com/salemkode/slp-explorer-backend) - Slp explorer for bitcoin cash. +- SLPDB [[code]](https://github.com/simpleledger/SLPDB) [[doc]](https://slp.dev/tooling/slpdb/) - simpleledger indexer +- [gs++](https://gs.fountainhead.cash/) [[code]](https://github.com/blockparty-sh/cpp_slp_graph_search) [[doc]](https://gs.fountainhead.cash/swagger.html) - a fast SLP indexer, validator, and graph search server. +- [SLP Stream](https://slpstream.fountainhead.cash/channel) [[code]](https://github.com/blockparty-sh/slpstream) [[doc]](https://slp.dev/tooling/slpstream/) - a frontend API for GS++ that provides a streaming output of new transactions. +- [goslp](https://github.com/simpleledgerinc/goslp) - SLP go libraries. +- [SLP Indexer](https://github.com/Bitcoin-com/slp-indexer) - bitcoin.com indexer. +- [SLP Icons](https://github.com/kosinusbch/slp-token-icons) - Hosted icons for slp tokens. + +## SLP Token Projects + +- **⭐ [PSF wallet](https://wallet.fullstack.cash/) ⭐** [[code]](https://github.com/Permissionless-Software-Foundation/gatsby-ipfs-web-wallet) - An web wallet with SLP support. +- [SLP Explorer](https://simpleledger.info/) [[code]](https://github.com/salemkode/slp-explorer) [[backend src]](https://github.com/salemkode/slp-explorer-backend) - Open source explorer for SLP tokens. +- Electron-Cash SLP Edition [[code]](https://github.com/simpleledger/Electron-Cash-SLP) [[releases]](https://github.com/simpleledger/Electron-Cash-SLP/releases) +- Honk Token [[archive]](https://web.archive.org/web/20230921212507/https://honk.cash/) [[whitepaper]](https://web.archive.org/web/20220409174235/https://www.honk.cash/whitepaper.pdf) - A gambling/gaming/multipurpose SLP token. +- mistcoin [[archive]](http://web.archive.org/web/20210128134553/https://mistcoin.org/) [[blue miner]](https://gitlab.com/blue_mist/miner) - A mineable SLP token using a proof-of-work covenant contract +- SpiceToken [[archive]](https://web.archive.org/web/20230216030610/https://spicetoken.org/) - A meme SLP token for social tipping. + + +tons of data from awesome monero: + +# Awesome Monero List + +A curated list of awesome Monero libraries, tools, and resources. + +## Contents + +- [Resources](#resources) +- [Wallets](#wallets) +- [Libraries](#libraries) +- [Docker](#docker) +- [Tools](#tools) +- [Nodes](#nodes) +- [Blockchain Explorers](#blockchain-explorers) +- [Built with Monero](#build-with-monero) +- [Mining](#mining) +- [Decentralized Exchanges](#decentralized-exchanges) +- [Atomic Swaps](#atomic-swaps) +- [Integrations](#integrations) +- [Merchants](#merchants) +- [Point of Sale](#point-of-sale) +- [Future development](#future-development) +- [Other](#other) + +## Resources + +- [Official Website](https://getmonero.org/) +- [Official GitHub](https://github.com/monero-project/monero) +- [Official Twitter](https://twitter.com/monero) +- [Official Reddit](https://www.reddit.com/r/Monero/) +- [Unofficial Docs](https://docs.monero.study/) +- [Monero Research Lab](https://github.com/monero-project/research-lab) + +- [Implementing Seraphis](https://raw.githubusercontent.com/UkoeHB/Seraphis/master/implementing_seraphis/Impl-Seraphis-0-0-2.pdf) +- [RandomX](https://github.com/tevador/RandomX) - RandomX is a proof-of-work (PoW) algorithm that is optimized for general-purpose CPUs. +- [LMDB](https://github.com/LMDB/lmdb) - Lightning Memory-Mapped Database + +### Books + +- [Mastering Monero](https://github.com/monerobook/monerobook) - "Mastering Monero: The future of private transactions" is your guide through the world of Monero, a leading cryptocurrency with a focus on private and censorship-resistant transactions. This book contains everything you need to know to start using Monero in your business or day-to-day life, even if you've never understood or interacted with cryptocurrencies before. +- [monero-book](https://github.com/Cuprate/monero-book) - This book aims to document the Monero protocol. Currently, work is being done to document Monero's consensus rules. This being completed as a part of [Cuprate](https://github.com/Cuprate/cuprate), the Rust Monero node. ([Website](https://monero-book.cuprate.org/)) + +## Wallets + +### Desktop Wallets + +- [Monero GUI Wallet](https://getmonero.org/downloads/) - Official desktop wallet +- [Feather Wallet](https://github.com/feather-wallet/feather) ([Website](https://featherwallet.org/)) - Lightweight desktop wallet +- [monero-wallet-generator](https://github.com/moneromooo-monero/monero-wallet-generator) - Self contained offline javacsript Monero wallet generator +- [Cake Wallet](https://github.com/cake-tech/cake_wallet) - Popular iOS and Android wallet and desktop wallet + +### Mobile Wallets + +- [Cake Wallet](https://github.com/cake-tech/cake_wallet) - Popular iOS and Android wallet and desktop wallet +- [Monerujo](https://github.com/m2049r/xmrwallet) - Popular Android wallet +- [Stack Wallet](https://github.com/cypherstack/stack_wallet) - A multicoin, cryptocurrency wallet +- [ANONERO](http://anonero.io/) - Hardened wallet with enforced privacy & security for Android (onion link) +- [MYSU](http://rk63tc3isr7so7ubl6q7kdxzzws7a7t6s467lbtw2ru3cwy6zu6w4jad.onion/) - A no-bullshit, pure Monero wallet suitable for both newcomers and experienced users. For Android. (onion link) + +### Hardware Wallets + +- [Kastelo](https://github.com/monero-project/kastelo) - This is the project to create an official Monero Hardware Wallet (Dead project) +- [passport2-monero](https://github.com/mjg-foundation/passport2-monero) - v2.x.x series of firmware for Passport, rebuilt for monero +- [MoneroSigner](https://github.com/Monero-HackerIndustrial/MoneroSigner) - Seedsigner Monero fork. Use an air-gapped Raspberry Pi Zero to sign monero transactions! +- [Monero Ledger App](https://github.com/LedgerHQ/app-monero) - Monero wallet application for Ledger Nano S and Nano X. (avoid buying Ledger products) + +### Other Wallets +- [Monero Subscriptions Wallet](https://github.com/lukeprofits/Monero_Subscriptions_Wallet) - A Monero wallet that automatically pays subscriptions. + +## Libraries + +- [monero-ts](https://github.com/woodser/monero-ts) - Monero TypeScript library for Node.js and browsers +- [monerophp](https://github.com/monero-integrations/monerophp) - A Monero library written in PHP by the Monero Integrations team. +- [monero-python](https://github.com/monero-integrations/monero-python) - A comprehensive Python module for handling Monero cryptocurrency +- [monero-rpc-php](https://github.com/refring/monero-rpc-php) - Monero daemon and wallet RPC client library written in modern PHP. +- [monero-java](https://github.com/woodser/monero-java) - Java library for using Monero +- [monero-rs](https://github.com/monero-rs/monero-rs) - Library with support for de/serialization on block data structures and key/address generation and scanning related to Monero cryptocurrency. +- [libmonero](https://github.com/monumexyz/libmonero) - libmonero is a library for the Monero cryptocurrency written in Rust. It is designed to be fast, safe and easy to use. +- [monero-cpp](https://github.com/woodser/monero-cpp) - C++ library for using Monero +- [go-monero-rpc-client](https://github.com/omani/go-monero-rpc-client) - A go client for the Monero wallet and daemon RPC +- [go-monero](https://github.com/duggavo/go-monero) - A multi-platform Go library for interacting with Monero servers either on clearnet or not, supporting daemon and wallet RPC, p2p commands and ZeroMQ. + +## Docker + +- [Simple Monerod Docker](https://github.com/sethforprivacy/simple-monerod-docker) - A simple docker image for running a Monero node. +- [Monero Suite](https://github.com/hundehausen/monero-suite) ([Website](https://monerosuite.org)) - Build your personal docker-compose.yml file for Monero services. +- [Docker-XMRig](https://github.com/metal3d/docker-xmrig) - Xmrig containeried to mine monero cryptocurrency +- [Moneroblock Docker](https://github.com/sethforprivacy/moneroblock-docker) - A simple and straightforward Dockerized MoneroBlock built from source and exposing standard ports. + +## Tools + +- [Monero Inflation Checker](https://github.com/DangerousFreedom1984/monero_inflation_checker) - Minimal Python tools and educational material for checking inflation in Monero. You can get more information at moneroinflation.com. +- [Monero Vanity Address Generator](https://github.com/hinto-janai/monero-vanity) - Monero vanity address generator for CPUs +- [monero-lws](https://github.com/vtnerd/monero-lws) - Monero Light Wallet Server (scans monero viewkeys and implements mymonero API) + +## Nodes + +- [Monero Node List](https://moneroworld.com/) - A list of public Monero nodes. +- [Monero Node Scanner](https://monerohash.com/nodes-distribution.html) - A tool to scan the Monero network for nodes. +- [monero.fail](https://monero.fail/) - Monero public node aggregator. +- [Monerod-in-Termux](https://github.com/CryptoGrampy/android-termux-monero-node) - Run a Monero Node on Android using Termux +- [check-monero-seed-nodes](https://github.com/plowsof/check-monero-seed-nodes) - A script to check the status of Monero seed nodes +- [Monero Node for Umbrel](https://github.com/deverickapollo/umbrel-monero) - Run a Monero node on your Umbrel personal server. +- [xmr.sh](https://github.com/vdo/xmr.sh) - xmr.sh script wizard sets up a new server running a monero node daemon with Docker compose, with your choice of SSL certificates for your domain, network selection, a Tor hidden service, Grafana dashboard and more. +- [Monero Nodo](https://github.com/MoneroNodo/Nodo) - Software running on a [Monero Nodo](https://moneronodo.com/): Monero Full Node on powerful hardware + +## Blockchain Explorers + +- [Onion Monero Blockchain Explorer](https://github.com/moneroexamples/onion-monero-blockchain-explorer) - A Monero blockchain explorer. +- [Moneroblock](https://github.com/duggavo/MoneroBlock) - Decentralized and trustless Monero block explorer + +## Built with Monero + +- [Nerostr](https://github.com/pluja/nerostr) - nostr paid relay, but with monero +- [NEVEKO](https://github.com/creating2morrow/neveko) - full-stack privacy application with gpg messaging, monero multisig and built-in i2p marketplace +- [Split My Lunch](https://github.com/AlexAnarcho/split-my-lunch) - Allow co-workers to split the lunch bill in Monero +- [XMR-T3-starter](https://gitlab.com/monero-studio/xmr-t3-starter) - A starter template for a T3 web app with monero-ts. t3-stack: nextjs (react), typescript, tailwind, trpc, prisma also includes: shadcn/ui, monero-ts + +## Mining + +- [XMRig](https://github.com/xmrig/xmrig) - High performance, open source, cross platform RandomX, CryptoNight and Argon2 CPU/GPU miner +- [Gupax](https://github.com/hinto-janai/gupax) - A simple GUI for mining Monero on P2Pool, using XMRig. +- [P2Pool](https://github.com/SChernykh/p2pool) - P2Pool is a decentralized Monero mining pool that works by creating a peer-to-peer network of miner nodes. +- [XMRig Proxy](https://github.com/xmrig/xmrig-proxy) - Stratum proxy with Web interface, support for several backup pools, and more. +- [Docker-XMRig](https://github.com/metal3d/docker-xmrig) - Xmrig containeried to mine monero cryptocurrency +- [MoneroOS](https://github.com/4rkal/MoneroOS) - Plug and play monero mining archuseriso config +- [XMRig for Android](https://github.com/XMRig-for-Android/xmrig-for-android) - ⛏ Mine Monero from your android device + +## Decentralized Exchanges + +- [Bisq](https://github.com/bisq-network/bisq) ([Website](https://bisq.network/)) - A decentralized exchange network for trading Monero and other cryptocurrencies. +- [Haveno](https://github.com/haveno-dex/haveno) - A decentralized, peer-to-peer, non-custodial Monero exchange for trading fiat currencies for Monero. +- [Serai](https://github.com/serai-dex/serai) - Serai is a new DEX, built from the ground up, initially planning on listing Bitcoin, Ethereum, DAI, and Monero, offering a liquidity-pool-based trading experience. Funds are stored in an economically secured threshold-multisig wallet. +- [BasicSwapDex](https://github.com/tecnovert/basicswap) ([Website](https://basicswapdex.com/)) - The BasicSwap DEX is a privacy-first and decentralized exchange which features cross-chain atomic swaps and a distributed order book. + +## Atomic Swaps + +- [XMR to BTC Atomic Swap](https://github.com/comit-network/xmr-btc-swap) - Bitcoin–Monero Cross-chain Atomic Swap +- [ETH-XMR Atomic Swaps](https://github.com/AthanorLabs/atomic-swap) - 💫 ETH-XMR atomic swap implementation +- [UnstoppableSwap GUI](https://github.com/UnstoppableSwap/unstoppableswap-gui) - Graphical User Interface (GUI) For Trustless Cross-Chain XMR<>BTC Atomic Swaps +- [BCH-XMR-SWAP PoC](https://github.com/PHCitizen/bch-xmr-swap) - A proof of concept for a Bitcoin Cash to Monero atomic swap +- [Farcaster Project](https://github.com/farcaster-project) - Farcaster is a cross-chain atomic swap protocol and implementation who allows to exchange Bitcoin and Monero in a peer-to-peer manner with anyone running a Farcaster node. +- [Samourai XMR-BTC Swap Beta](https://code.samourai.io/wallet/comit-swaps-java) - A GUI for COMIT XMR-BTC atomic swaps with modifications to further enhance anonymity, with the Automated Swap Backend (ASB) built-in, as well as Samourai Wallet Whirlpool for automatic mixing of redeemed BTC. (Beta!) + + +## Merchants + +- [Monero Merchants](https://www.monerooutreach.org/stories/monero_merchants.html) - A list of merchants that accept Monero as payment. +- [Monerica](https://github.com/monerica-project/monerica) ([Website](https://monerica.com/)) - A directory for a Monero circular economy +- [Monero for Merchants](https://github.com/ASchmidt1024/monero-for-merchants-booklet) - A printable booklet to attract merchants to accept Monero (multiple languages!) + +## Point of Sale + +- [Kasisto](https://github.com/amiuhle/kasisto) - A Monero Point of Sale payment system +- [Monero Gateway for WooCommerce](https://github.com/monero-integrations/monerowp) - A Monero WooCommerce Plugin for Wordpress +- [MoneroPay](https://github.com/moneropay/moneropay) - A Monero payment gateway for WooCommerce +- [Monero Merchant](https://github.com/RuiSiang/monero-merchant) - Monero Merchant is a RESTful API wrapper for the official Monero wallet RPC. This project is mainly for merchants who hope to accept Monero as payment. +- [AcceptXMR](https://github.com/busyboredom/acceptxmr) - This library aims to provide a simple, reliable, and efficient means to track monero payments. +- [HotShop](https://github.com/CryptoGrampy/HotShop) - An Ephemeral, browser-based, no-private-key, no-server Point of Sale for receiving and validating Monero payments. Repository is archived :( +- [monerochan-merchant-rpc](https://github.com/spirobel/monerochan-merchant-rpc) - A tool to accept digital cash at your online business. + +## Future development + +- [Seraphis](https://github.com/UkoeHB/Seraphis) - Seraphis is a privacy-focused transaction protocol for p2p electronic cash systems (e.g. cryptocurrencies). +- [Full chain membership proofs](https://github.com/kayabaNerve/full-chain-membership-proofs) +- [Cuprate](https://github.com/Cuprate/cuprate) - an upcoming experimental, modern & secure monero node. Written in Rust. +- [wallet3](https://github.com/seraphis-migration/wallet3) - Info and discussions about a hypothetical full 'wallet2' rewrite from scratch diff --git a/message_parser_wasm/src/lib.rs b/message_parser_wasm/src/lib.rs index cb6d75f..e6882ee 100644 --- a/message_parser_wasm/src/lib.rs +++ b/message_parser_wasm/src/lib.rs @@ -23,7 +23,7 @@ pub fn parse_text(s: &str, enable_markdown: bool) -> JsValue { serde_wasm_bindgen::to_value(&ast).expect("Element converts to JsValue") } -/// parses text to json AST (text elements and labled links, to replicate current desktop implementation) +/// parses text to json AST (text elements and labeled links, to replicate current desktop implementation) #[wasm_bindgen] pub fn parse_desktop_set(s: &str) -> JsValue { serde_wasm_bindgen::to_value(&deltachat_message_parser::parser::parse_desktop_set(s)) diff --git a/rust-toolchain b/rust-toolchain index 9405730..369f996 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.64.0 +1.77.2 diff --git a/spec.md b/spec.md index 8975c73..3d07967 100644 --- a/spec.md +++ b/spec.md @@ -41,6 +41,7 @@ Make email addresses clickable, opens the chat with that contact and creates it Make URLs clickable. - detect all valid hyperlink URLs that have the `://` (protocol://host). + - according to [RFC3987](https://www.rfc-editor.org/rfc/rfc3987) and [RFC3988](https://www.rfc-editor.org/rfc/rfc3988) - other links like `mailto:` (note there is just a single `:`, no `://`) will get separate parsing that includes a whitelisted protocol name, otherwise there will likely be unexpected behavior if user types `hello:world` - will be recognized as link. diff --git a/src/lib.rs b/src/lib.rs index 20ec82a..906aecf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ clippy::get_last_with_len, clippy::get_unwrap, clippy::get_unwrap, - clippy::integer_arithmetic, + clippy::arithmetic_side_effects, clippy::match_on_vec_items, clippy::match_wild_err_arm, clippy::missing_panics_doc, diff --git a/src/parser/link_url.rs b/src/parser/link_url.rs deleted file mode 100644 index ff9a57b..0000000 --- a/src/parser/link_url.rs +++ /dev/null @@ -1,543 +0,0 @@ -use nom::{ - branch::alt, - bytes::complete::{tag, take, take_till1, take_while, take_while1}, - character::complete::char, - character::complete::digit1, - combinator::{consumed, opt, recognize}, - error::{ErrorKind, ParseError}, - multi::many0, - sequence::delimited, - sequence::tuple, - AsChar, IResult, -}; - -use super::parse_from_text::base_parsers::{is_not_white_space, CustomError}; - -///! Parsing / Validation of URLs -/// -/// - hyperlinks (:// scheme) -/// - whitelisted scheme (: scheme) -/// -/// for hyperlinks it also checks whether the domain contains punycode - -// There are two kinds of Urls -// - Common Internet Scheme https://datatracker.ietf.org/doc/html/rfc1738#section-3.1 -// - Every other url (like mailto) - -#[derive(Debug, PartialEq, Eq, Serialize)] -pub struct LinkDestination<'a> { - pub target: &'a str, - /// hostname if it was found - pub hostname: Option<&'a str>, - /// contains data for the punycode warning if punycode was detected - /// (the host part contains non ascii unicode characters) - pub punycode: Option, - /// scheme - pub scheme: &'a str, -} - -#[derive(Debug, PartialEq, Eq, Serialize)] -pub struct PunycodeWarning { - original_hostname: String, - ascii_hostname: String, - punycode_encoded_url: String, -} - -/// determines which generic schemes (without '://') get linkifyed -fn is_allowed_generic_scheme(scheme: &str) -> bool { - matches!( - scheme.to_ascii_lowercase().as_ref(), - "mailto" - | "news" - | "feed" - | "tel" - | "sms" - | "geo" - | "maps" - | "bitcoin" - | "bitcoincash" - | "eth" - | "ethereum" - | "magnet" - ) -} - -impl LinkDestination<'_> { - /// parse a link that is not in a delimited link or a labled link, just a part of normal text - /// it has a whitelist of schemes, because otherwise - pub(crate) fn parse_standalone_with_whitelist( - input: &str, - ) -> IResult<&str, LinkDestination, CustomError<&str>> { - if let Ok((rest, (link, info))) = parse_url(input) { - let (hostname, punycode, scheme) = match info { - UrlInfo::CommonInternetSchemeURL { - has_puny_code_in_host_name, - hostname, - ascii_hostname, - scheme, - } => { - if has_puny_code_in_host_name { - ( - Some(hostname), - Some(PunycodeWarning { - original_hostname: hostname.to_owned(), - punycode_encoded_url: link.replacen(hostname, &ascii_hostname, 1), - ascii_hostname, - }), - scheme, - ) - } else { - (Some(hostname), None, scheme) - } - } - UrlInfo::GenericUrl { scheme } => { - if !is_allowed_generic_scheme(scheme) { - return Err(nom::Err::Error(CustomError::InvalidLink)); - } - (None, None, scheme) - } - }; - - Ok(( - rest, - LinkDestination { - target: link, - hostname, - punycode, - scheme, - }, - )) - } else { - Err(nom::Err::Error(CustomError::InvalidLink)) - } - } - - pub fn parse(input: &str) -> IResult<&str, LinkDestination, CustomError<&str>> { - if let Ok((rest, (link, info))) = parse_url(input) { - let (hostname, punycode, scheme) = match info { - UrlInfo::CommonInternetSchemeURL { - has_puny_code_in_host_name, - hostname, - ascii_hostname, - scheme, - } => { - if has_puny_code_in_host_name { - ( - Some(hostname), - Some(PunycodeWarning { - original_hostname: hostname.to_owned(), - punycode_encoded_url: link.replacen(hostname, &ascii_hostname, 1), - ascii_hostname, - }), - scheme, - ) - } else { - (Some(hostname), None, scheme) - } - } - UrlInfo::GenericUrl { scheme, .. } => (None, None, scheme), - }; - - Ok(( - rest, - LinkDestination { - target: link, - hostname, - punycode, - scheme, - }, - )) - } else { - Err(nom::Err::Error(CustomError::InvalidLink)) - } - } -} - -#[derive(Debug, PartialEq)] -enum UrlInfo<'a> { - /// wether url is an Common Internet Scheme URL (if it has `://`) - CommonInternetSchemeURL { - has_puny_code_in_host_name: bool, - hostname: &'a str, - ascii_hostname: String, - scheme: &'a str, - }, - GenericUrl { - scheme: &'a str, - }, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum LinkParseError { - Nom(I, ErrorKind), - ThisIsNotPercentEncoding, -} - -impl ParseError for LinkParseError { - fn from_error_kind(input: I, kind: ErrorKind) -> Self { - LinkParseError::Nom(input, kind) - } - - fn append(_: I, _: ErrorKind, other: Self) -> Self { - other - } -} - -fn is_reserved(char: char) -> bool { - matches!(char, ';' | '/' | '?' | ':' | '@' | '&' | '=') -} - -fn is_hex_digit(c: char) -> bool { - c.is_ascii_hexdigit() -} - -fn escaped_char(input: &str) -> IResult<&str, &str, LinkParseError<&str>> { - let (input, content) = take(3usize)(input)?; - let mut content_chars = content.chars(); - - if content_chars.next() == Some('%') - && content_chars.next().map(is_hex_digit) == Some(true) - && content_chars.next().map(is_hex_digit) == Some(true) - { - Ok((input, content)) - } else { - Err(nom::Err::Error(LinkParseError::ThisIsNotPercentEncoding)) - } -} - -fn is_safe(char: char) -> bool { - matches!(char, '$' | '-' | '_' | '.' | '+') -} - -fn is_extra(char: char) -> bool { - matches!( - char, - '!' | '*' | '\'' | '(' | ')' | ',' | '{' | '}' | '[' | ']' | '<' | '>' - ) -} - -fn is_unreserved(char: char) -> bool { - char.is_alphanum() || is_safe(char) || is_extra(char) -} - -fn x_char_sequence(input: &str) -> IResult<&str, &str, LinkParseError<&str>> { - //xchar = unreserved | reserved | escape - recognize(many0(alt(( - take_while1(is_unreserved), - take_while1(is_reserved), - escaped_char, - tag("#"), - ))))(input) -} - -fn scheme_char(char: char) -> bool { - //; the scheme is in lower case; interpreters should use case-ignore - //scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] - match char { - '+' | '-' | '.' => true, - _ => char.is_alphanum(), - } -} - -fn is_user_or_password_char(char: char) -> bool { - match char { - ';' | '?' | '&' | '=' => true, - _ => is_unreserved(char), - } -} - -fn user_or_password(input: &str) -> IResult<&str, &str, LinkParseError<&str>> { - recognize(many0(alt(( - take_while(is_user_or_password_char), - escaped_char, - ))))(input) -} - -fn login(input: &str) -> IResult<&str, (), LinkParseError<&str>> { - // login = user [ ":" password ] "@" - let (input, _) = user_or_password(input)?; - let (input, _) = opt(tuple((char(':'), user_or_password)))(input)?; - let (input, _) = char('@')(input)?; - Ok((input, ())) -} - -fn is_ipv6_char(char: char) -> bool { - match char { - ':' => true, - _ => is_hex_digit(char), - } -} - -fn is_alphanum_or_hyphen_minus(char: char) -> bool { - match char { - '-' => true, - _ => char.is_alphanum(), - } -} -fn is_forbidden_in_idnalabel(char: char) -> bool { - is_reserved(char) || is_extra(char) || char == '>' -} - -/// creates possibility for punycodedecoded/unicode/internationalized domains -/// takes everything until reserved, extra or '>' -fn idnalabel(input: &str) -> IResult<&str, &str, LinkParseError<&str>> { - let (input, label) = take_till1(is_forbidden_in_idnalabel)(input)?; - Ok((input, label)) -} - -fn host<'a>(input: &'a str) -> IResult<&'a str, (&'a str, bool), LinkParseError<&'a str>> { - if let Ok((input, host)) = recognize::<_, _, LinkParseError<&'a str>, _>(delimited( - char('['), - take_while1(is_ipv6_char), - char(']'), - ))(input) - { - // ipv6 hostnumber - // sure the parsing here could be more specific and correct -> TODO - Ok((input, (host, true))) - } else if let Ok((input, host)) = recognize::<_, _, LinkParseError<&'a str>, _>(tuple(( - digit1, - char('.'), - digit1, - char('.'), - digit1, - char('.'), - digit1, - )))(input) - { - // ipv4 hostnumber - // sure the parsing here could be more specific and correct -> TODO - Ok((input, (host, false))) - } else { - // idna hostname (valid chars until ':' or '/') - // sure the parsing here could be more specific and correct -> TODO - let (input, host) = - recognize(tuple((many0(tuple((idnalabel, char('.')))), idnalabel)))(input)?; - Ok((input, (host, false))) - } -} - -fn punycode_encode(host: &str) -> String { - host.split('.') - .map(|sub| { - let mut has_non_ascii_char = false; - for char in sub.chars() { - if !is_alphanum_or_hyphen_minus(char) { - has_non_ascii_char = true; - break; - } - } - if has_non_ascii_char { - format!( - "xn--{}", - unic_idna_punycode::encode_str(sub) - .unwrap_or_else(|| "[punycode encode failed]".to_owned()) - ) - } else { - sub.to_owned() - } - }) - .collect::>() - .join(".") -} - -fn url_intern<'a>(input: &'a str) -> IResult<&'a str, UrlInfo<'a>, LinkParseError<&'a str>> { - let (input, scheme) = take_while1(scheme_char)(input)?; - let (input, _) = tag(":")(input)?; - - if let Ok((input, _)) = tag::<&'a str, &'a str, LinkParseError<&'a str>>("//")(input) { - // ip-schemepart - // parse login - let (input, _) = opt(login)(input)?; - // parse host - let (input, (host, is_ipv6)) = host(input)?; - // parse port - let (input, _) = opt(tuple((char(':'), digit1)))(input)?; - // parse urlpath - let (input, _) = opt(tuple(( - alt((char('/'), char('?'), char('#'))), - x_char_sequence, - )))(input)?; - - let is_puny = if is_ipv6 { - false - } else { - let mut is_puny = false; - for char in host.chars() { - if !(is_alphanum_or_hyphen_minus(char) || char == '.') { - is_puny = true; - break; - } - } - is_puny - }; - - Ok(( - input, - UrlInfo::CommonInternetSchemeURL { - scheme, - hostname: host, - has_puny_code_in_host_name: is_puny, - ascii_hostname: if is_puny { - punycode_encode(host) - } else { - host.to_string() - }, - }, - )) - } else { - // schemepart - let (input, _) = take_while(is_not_white_space)(input)?; - - Ok((input, UrlInfo::GenericUrl { scheme })) - } -} - -fn parse_url(input: &str) -> IResult<&str, (&str, UrlInfo), LinkParseError<&str>> { - consumed(url_intern)(input) -} - -// TODO testcases - -// ipv6 https://[::1]/ - -// invalid ascii domain (without non ascii char: https://-test-/hi ) - -#[cfg(test)] -mod test { - #![allow(clippy::unwrap_used)] - use crate::parser::link_url::{parse_url, punycode_encode, UrlInfo}; - - #[test] - fn basic_parsing() { - let test_cases = vec![ - "http://delta.chat", - "http://delta.chat:8080", - "http://localhost", - "http://127.0.0.0", - "https://[::1]/", - "https://[::1]:9000?hi#o", - "https://delta.chat", - "ftp://delta.chat", - "https://delta.chat/en/help", - "https://delta.chat/en/help?hi=5&e=4", - "https://delta.chat?hi=5&e=4", - "https://delta.chat/en/help?hi=5&e=4#section2.0", - "https://delta#section2.0", - "http://delta.chat:8080?hi=5&e=4#section2.0", - "http://delta.chat:8080#section2.0", - "mailto:delta@example.com", - "mailto:delta@example.com?subject=hi&body=hello%20world", - "mailto:foö@ü.chat", - "https://ü.app#help", - "ftp://test-test", - "http://münchen.de", - ]; - - for input in &test_cases { - // println!("testing {}", input); - - let (rest, (url, _)) = parse_url(input).unwrap(); - - assert_eq!(input, &url); - assert_eq!(rest.len(), 0); - } - } - - #[test] - fn invalid_domains() { - let test_cases = vec![";?:/hi", "##://thing"]; - - for input in &test_cases { - // println!("testing {}", input); - assert!(parse_url(input).is_err()); - } - } - #[test] - fn punycode_encode_fn() { - assert_eq!(punycode_encode("münchen.de"), "xn--mnchen-3ya.de") - } - - #[test] - fn punycode_detection() { - assert_eq!( - parse_url("http://münchen.de").unwrap().1, - ( - "http://münchen.de", - UrlInfo::CommonInternetSchemeURL { - hostname: "münchen.de", - has_puny_code_in_host_name: true, - ascii_hostname: "xn--mnchen-3ya.de".to_owned(), - scheme: "http" - } - ) - ); - - assert_eq!( - parse_url("http://muenchen.de").unwrap().1, - ( - "http://muenchen.de", - UrlInfo::CommonInternetSchemeURL { - hostname: "muenchen.de", - has_puny_code_in_host_name: false, - ascii_hostname: "muenchen.de".to_owned(), - scheme: "http" - } - ) - ); - } - - #[test] - fn common_schemes() { - assert_eq!( - parse_url("http://delta.chat").unwrap().1, - ( - "http://delta.chat", - UrlInfo::CommonInternetSchemeURL { - hostname: "delta.chat", - has_puny_code_in_host_name: false, - ascii_hostname: "delta.chat".to_owned(), - scheme: "http" - } - ) - ); - assert_eq!( - parse_url("https://delta.chat").unwrap().1, - ( - "https://delta.chat", - UrlInfo::CommonInternetSchemeURL { - hostname: "delta.chat", - has_puny_code_in_host_name: false, - ascii_hostname: "delta.chat".to_owned(), - scheme: "https" - } - ) - ); - } - #[test] - fn generic_schemes() { - assert_eq!( - parse_url("mailto:someone@example.com").unwrap().1, - ( - "mailto:someone@example.com", - UrlInfo::GenericUrl { scheme: "mailto" } - ) - ); - assert_eq!( - parse_url("bitcoin:bc1qt3xhfvwmdqvxkk089tllvvtzqs8ts06u3u6qka") - .unwrap() - .1, - ( - "bitcoin:bc1qt3xhfvwmdqvxkk089tllvvtzqs8ts06u3u6qka", - UrlInfo::GenericUrl { scheme: "bitcoin" } - ) - ); - assert_eq!( - parse_url("geo:37.786971,-122.399677").unwrap().1, - ( - "geo:37.786971,-122.399677", - UrlInfo::GenericUrl { scheme: "geo" } - ) - ); - } -} diff --git a/src/parser/link_url/ip/ip_literal.rs b/src/parser/link_url/ip/ip_literal.rs new file mode 100644 index 0000000..0efaf7b --- /dev/null +++ b/src/parser/link_url/ip/ip_literal.rs @@ -0,0 +1,12 @@ +use nom::{ + branch::alt, character::complete::char, combinator::recognize, sequence::tuple, IResult, +}; + +use crate::parser::{ + link_url::ip::{ipv6::ipv6, ipvfuture::ipvfuture}, + parse_from_text::base_parsers::CustomError, +}; + +pub fn ip_literal(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(tuple((char('['), alt((ipv6, ipvfuture)), char(']'))))(input) +} diff --git a/src/parser/link_url/ip/ipv4.rs b/src/parser/link_url/ip/ipv4.rs new file mode 100644 index 0000000..0012556 --- /dev/null +++ b/src/parser/link_url/ip/ipv4.rs @@ -0,0 +1,14 @@ +use nom::{ + character::complete::{char, u8}, + combinator::recognize, + sequence::tuple, + IResult, +}; + +use crate::parser::parse_from_text::base_parsers::CustomError; + +pub fn ipv4(input: &str) -> IResult<&str, &str, CustomError<&str>> { + let (input, ipv4_) = + recognize(tuple((u8, char('.'), u8, char('.'), u8, char('.'), u8)))(input)?; + Ok((input, ipv4_)) +} diff --git a/src/parser/link_url/ip/ipv6.rs b/src/parser/link_url/ip/ipv6.rs new file mode 100644 index 0000000..7e0b05c --- /dev/null +++ b/src/parser/link_url/ip/ipv6.rs @@ -0,0 +1,89 @@ +use nom::{ + branch::alt, + bytes::complete::{tag, take_while_m_n}, + character::complete::char, + combinator::{opt, recognize}, + multi::{count, many_m_n}, + sequence::tuple, + IResult, +}; + +use crate::parser::{parse_from_text::base_parsers::CustomError, utils::is_hex_digit}; + +use super::ipv4::ipv4; + +fn h16(input: &str) -> IResult<&str, &str, CustomError<&str>> { + take_while_m_n(1, 4, is_hex_digit)(input) +} + +// consume or an ipv4 +fn ls32(input: &str) -> IResult<&str, &str, CustomError<&str>> { + let result = recognize(tuple((h16, char(':'), h16)))(input); + if result.is_err() { + ipv4(input) + } else { + result + } +} + +fn h16_and_period(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(tuple((h16, char(':'))))(input) +} + +fn double_period(input: &str) -> IResult<&str, &str, CustomError<&str>> { + tag("::")(input) +} + +pub fn ipv6(input: &str) -> IResult<&str, &str, CustomError<&str>> { + // an IPv6 is one of these: + alt(( + // <6 h16_and_period> + recognize(tuple((count(h16_and_period, 6), ls32))), + // :: <5 h16_and_period> + recognize(tuple((double_period, many_m_n(5, 5, h16_and_period), ls32))), + // [h16] :: <4 h16_and_period> + recognize(tuple(( + opt(h16), + double_period, + count(h16_and_period, 4), + ls32, + ))), + // [h16_and_period] :: <3*h16_and_period> + recognize(tuple(( + opt(tuple((many_m_n(0, 1, h16_and_period),))), + double_period, + count(h16_and_period, 3), + ls32, + ))), + // [<0 to 2 h16_and_period> ] :: <2*h16_and_period> + recognize(tuple(( + opt(tuple((many_m_n(0, 2, h16_and_period), h16))), + double_period, + count(h16_and_period, 2), + ls32, + ))), + // [<0 to 3 h16_and_period>] :: + recognize(tuple(( + opt(tuple((many_m_n(0, 3, h16_and_period), h16))), + double_period, + ls32, + ))), + // [<0 to 4 h16_and_period>] :: + recognize(tuple(( + opt(tuple((many_m_n(0, 4, h16_and_period), h16))), + double_period, + ls32, + ))), + // [<0 to 5 h16_and_period>] :: + recognize(tuple(( + opt(tuple((many_m_n(0, 5, h16_and_period), h16))), + double_period, + h16, + ))), + // [<0 to 6 h16_and_period>] :: + recognize(tuple(( + opt(tuple((many_m_n(0, 6, h16_and_period), h16))), + double_period, + ))), + ))(input) +} diff --git a/src/parser/link_url/ip/ipvfuture.rs b/src/parser/link_url/ip/ipvfuture.rs new file mode 100644 index 0000000..78a68e1 --- /dev/null +++ b/src/parser/link_url/ip/ipvfuture.rs @@ -0,0 +1,22 @@ +use nom::{ + bytes::complete::take_while_m_n, character::complete::char, combinator::recognize, + sequence::tuple, IResult, +}; + +use crate::parser::{ + parse_from_text::base_parsers::CustomError, + utils::{is_hex_digit, is_sub_delim, is_unreserved}, +}; + +fn is_ipvfuture_last(ch: char) -> bool { + is_sub_delim(ch) || is_unreserved(ch) || ch == ':' +} + +pub fn ipvfuture(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(tuple(( + char('v'), + take_while_m_n(1, 1, is_hex_digit), + char('.'), + take_while_m_n(1, 1, is_ipvfuture_last), + )))(input) +} diff --git a/src/parser/link_url/ip/mod.rs b/src/parser/link_url/ip/mod.rs new file mode 100644 index 0000000..8ba551d --- /dev/null +++ b/src/parser/link_url/ip/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod ip_literal; +pub(crate) mod ipv4; +mod ipv6; +mod ipvfuture; diff --git a/src/parser/link_url/mod.rs b/src/parser/link_url/mod.rs new file mode 100644 index 0000000..9473ba2 --- /dev/null +++ b/src/parser/link_url/mod.rs @@ -0,0 +1,85 @@ +mod ip; +mod parse_link; + +use nom::{ + error::{ErrorKind, ParseError}, + IResult, Slice, +}; + +use crate::parser::{link_url::parse_link::parse_link, parse_from_text::base_parsers::CustomError}; + +/* Parsing / Validation of URLs + * + * - hyperlinks (:// scheme) according to RFC3987 and RFC3988 + * - whitelisted scheme (: scheme) according to our own simple thing :) + * + * for hyperlinks it also checks whether the domain contains punycode + * + * There are two kinds of Urls + * - Common Internet Scheme[1] + * - Every other url (like mailto) + * [1] RFC1738(Section 3.1), RFC3987, RFC3988 --Farooq + */ + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct LinkDestination<'a> { + pub target: &'a str, + /// hostname if it was found + pub hostname: Option<&'a str>, + /// contains data for the punycode warning if punycode was detected + /// (the host part contains non ascii unicode characters) + pub punycode: Option, + /// scheme + pub scheme: &'a str, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct PunycodeWarning { + pub original_hostname: String, + pub ascii_hostname: String, + pub punycode_encoded_url: String, +} + +impl LinkDestination<'_> { + /// parse a link that is not in a delimited link or a labled link, just a part of normal text + /// + /// - for generic schemes (schemes without `://`) this uses a whitelist not reduce false positives + /// - it also ignores the last punctuation sign if it is at the end of the link + pub fn parse(input: &str) -> IResult<&str, LinkDestination, CustomError<&str>> { + if let Ok((rest, link_destination)) = parse_link(input) { + Ok((rest, link_destination)) + } else { + Err(nom::Err::Error(CustomError::InvalidLink)) + } + } + + // This is for parsing markdown labelled links. + pub fn parse_labelled(input: &str) -> IResult<&str, LinkDestination, CustomError<&str>> { + let (mut remaining, mut link) = Self::parse(input)?; + if let Some(first) = remaining.chars().next() { + if matches!(first, ';' | '.' | ',' | ':') { + // ^ markdown labelled links can include one of these characters at the end + // and it's therefore part of the link + let point = link.target.len().saturating_add(1); + link.target = input.slice(..point); + remaining = input.slice(point..); + } + } + Ok((remaining, link)) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum LinkParseError { + Nom(I, ErrorKind), +} + +impl ParseError for LinkParseError { + fn from_error_kind(input: I, kind: ErrorKind) -> Self { + LinkParseError::Nom(input, kind) + } + + fn append(_: I, _: ErrorKind, other: Self) -> Self { + other + } +} diff --git a/src/parser/link_url/parse_link.rs b/src/parser/link_url/parse_link.rs new file mode 100644 index 0000000..9ff1d93 --- /dev/null +++ b/src/parser/link_url/parse_link.rs @@ -0,0 +1,438 @@ +use std::ops::RangeInclusive; + +use nom::{ + branch::alt, + bytes::complete::{tag, take_while, take_while1, take_while_m_n}, + character::complete::char, + combinator::{opt, recognize}, + multi::{many0, many1}, + sequence::tuple, + IResult, Slice, +}; + +use crate::parser::{ + link_url::{ + ip::{ip_literal::ip_literal, ipv4::ipv4}, + LinkDestination, PunycodeWarning, + }, + parse_from_text::base_parsers::CustomError, + utils::{ + is_alpha, is_digit, is_hex_digit, is_in_one_of_ranges, is_not_white_space, is_sub_delim, + is_unreserved, + }, +}; + +/// determines which generic schemes (without '://') get linkifyed +fn is_allowed_generic_scheme(scheme: &str) -> bool { + matches!( + scheme.to_ascii_lowercase().as_ref(), + "mailto" + | "news" + | "feed" + | "tel" + | "sms" + | "geo" + | "maps" + | "bitcoin" + | "bitcoincash" + | "eth" + | "ethereum" + | "magnet" + ) +} + +// These ranges have been extracted from RFC3987, Page 8. +const UCSCHAR_RANGES: [RangeInclusive; 17] = [ + 0xa0..=0xd7ff, + 0xF900..=0xFDCF, + 0xFDF0..=0xFFEF, + 0x10000..=0x1FFFD, + 0x20000..=0x2FFFD, + 0x30000..=0x3FFFD, + 0x40000..=0x4FFFD, + 0x50000..=0x5FFFD, + 0x60000..=0x6FFFD, + 0x70000..=0x7FFFD, + 0x80000..=0x8FFFD, + 0x90000..=0x9FFFD, + 0xA0000..=0xAFFFD, + 0xB0000..=0xBFFFD, + 0xC0000..=0xCFFFD, + 0xD0000..=0xDFFFD, + 0xE1000..=0xEFFFD, +]; + +fn is_ucschar(c: char) -> bool { + is_in_one_of_ranges(c as u32, &UCSCHAR_RANGES[..]) +} + +fn is_iunreserved(c: char) -> bool { + is_unreserved(c) || is_ucschar(c) +} + +// Here again, order is important. As URLs/IRIs have letters in them +// most of the time and less digits or other characters. --Farooq +fn is_scheme(c: char) -> bool { + is_alpha(c) || is_digit(c) || is_other_scheme(c) +} + +fn is_other_scheme(c: char) -> bool { + matches!(c, '+' | '-' | '.') +} + +fn is_ireg_name_not_pct_encoded(c: char) -> bool { + is_iunreserved(c) || is_sub_delim(c) +} + +/// Parse host +/// +/// # Description +/// +/// Parse host. Returns the rest, the host string and a boolean indicating +/// if it is IPvFuture or IPv6. +/// +/// A host is either an IP-Literal(IPv6 or vFuture) or an +/// IPv4 or an Ireg name(e.g. far.chickenkiller.com :) +/// +/// # Return value +/// - `(host, true)` if host is IP-Literal +/// - `(host, false)` if it's ipv4 or ireg-name +fn parse_host(input: &str) -> IResult<&str, (&str, bool), CustomError<&str>> { + match ip_literal(input) { + Ok((input, host)) => { + // It got parsed, then it's an IP Literal meaning + // it's either IPv6 or IPvFuture + Ok((input, (host, true))) + } + Err(..) => { + let (input, host) = alt((ipv4, take_while_ireg))(input)?; + Ok((input, (host, false))) + } + } +} + +fn take_while_ireg(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(many0(alt(( + recognize(many1(take_while_pct_encoded)), + take_while1(is_ireg_name_not_pct_encoded), + ))))(input) +} + +/// Parse the iauthority block +/// # Description +/// An iauthority is... +/// `[iuserinfo] [:port]` +/// # Return value +/// unconsumed string AND `(iauthority, host, is_ipliteral)` where `ipliteral` is a boolean +fn iauthority(input: &str) -> IResult<&str, (&str, &str, bool), CustomError<&str>> /* (iauthority, host, bool) */ +{ + let i = <&str>::clone(&input); + let (input, userinfo) = opt(recognize(tuple((take_while_iuserinfo, char('@')))))(input)?; + let (input, (host, is_ipv6_or_future)) = parse_host(input)?; + let (input, port) = opt(recognize(tuple((char(':'), take_while(is_digit)))))(input)?; + let userinfo = userinfo.unwrap_or(""); + let port = port.unwrap_or(""); + let len = userinfo.len().saturating_add(port.len()); + if let Some(out) = i.get(0..len) { + Ok((input, (out, host, is_ipv6_or_future))) + } else { + Err(nom::Err::Failure(CustomError::NoContent)) + } +} + +/// Consume an iuserinfo +fn take_while_iuserinfo(input: &str) -> IResult<&str, &str, CustomError<&str>> { + alt(( + recognize(many0(take_while_pct_encoded)), + take_while(is_iuserinfo_not_pct_encoded), + ))(input) +} + +fn is_iuserinfo_not_pct_encoded(c: char) -> bool { + is_iunreserved(c) || is_sub_delim(c) || c == ':' +} + +fn is_ipchar_not_pct_encoded(c: char) -> bool { + is_iunreserved(c) || is_sub_delim(c) || matches!(c, ':' | '@') +} + +fn take_while_ipchar(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(many0(alt(( + take_while(is_ipchar_not_pct_encoded), + take_while_pct_encoded, + ))))(input) +} + +fn take_while_ipchar1(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(many1(alt(( + take_while1(is_ipchar_not_pct_encoded), + take_while_pct_encoded, + ))))(input) +} + +const IPRIVATE_RANGES: [RangeInclusive; 3] = + [0xe000..=0xf8ff, 0xf0000..=0xffffd, 0x100000..=0x10fffd]; + +fn is_iprivate(c: char) -> bool { + is_in_one_of_ranges(c as u32, &IPRIVATE_RANGES[..]) +} + +fn is_iquery_not_pct_encoded(c: char) -> bool { + is_iprivate(c) || is_ipchar_not_pct_encoded(c) || matches!(c, '/' | '?') +} + +/// Consume an iquery block +fn iquery(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(many0(alt(( + take_while1(is_iquery_not_pct_encoded), + take_while_pct_encoded, + ))))(input) +} + +fn take_while_ifragment(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(many0(alt((take_while_ipchar1, tag("/"), tag("?")))))(input) +} + +/// Consume scheme characters from input +/// +/// # Description +/// This function as it can be seen, consumes exactly an alpha and as many +/// scheme characters as there are. then it gets a slice of input(as cloned to i) +/// +/// # Arguments +/// +/// - `input` the input string +/// +/// # Return value +/// (unconsumed input AND the scheme string in order) OR Error +fn scheme(input: &str) -> IResult<&str, &str, CustomError<&str>> { + let i = <&str>::clone(&input); + let (input, _first) = take_while_m_n(1, 1, is_alpha)(input)?; + let (input, second) = take_while(is_scheme)(input)?; + let len = 1usize.saturating_add(second.len()); + // "1" is for the first, its length is always 1 + if let Some(out) = i.get(0..len) { + Ok((input, out)) + } else { + Err(nom::Err::Failure(CustomError::NoContent)) + } +} + +/// Take as many pct encoded blocks as there are. a block is %XX where X is a hex digit +fn take_while_pct_encoded(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(many1(tuple(( + char('%'), + take_while_m_n(2, 2, is_hex_digit), + ))))(input) +} + +/// encode a host to punycode encoded string +fn punycode_encode(host: &str) -> String { + host.split('.') + .map(|sub| { + if is_puny(sub) { + format!( + "xn--{}", + unic_idna_punycode::encode_str(sub) + .unwrap_or_else(|| "[punycode encode failed]".to_owned()) + ) + } else { + sub.to_owned() + } + }) + .collect::>() + .join(".") +} + +/// Returns true if host string contains non ASCII characters +fn is_puny(host: &str) -> bool { + for ch in host.chars() { + if !(ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-')) { + return true; + } + } + false +} + +/// Return a PunycodeWarning struct if host need punycode encoding else None +pub fn get_puny_code_warning(link: &str, host: &str) -> Option { + if is_puny(host) { + let ascii_hostname = punycode_encode(host); + Some(PunycodeWarning { + original_hostname: host.to_owned(), + ascii_hostname: ascii_hostname.to_owned(), + punycode_encoded_url: link.replacen(host, &ascii_hostname, 1), + }) + } else { + None + } +} + +fn ifragment(input: &str) -> IResult<&str, &str, CustomError<&str>> { + recognize(tuple((char('#'), take_while_ifragment)))(input) +} + +macro_rules! link_correct { + ($a: expr, $b: expr, $c: expr, $d: expr) => { + // for opening ones + { + $a = $a.saturating_add(1); + if $d.slice($c..).find($b).is_none() { + return Some($c); + } + } + }; + ($a: expr, $b: expr) => { + // for closing ones + { + if $a == 0 { + return Some($b); + } else { + $a = $a.saturating_sub(1); + } + } + }; +} + +// TODO: better name for this function +fn get_correct_link(link: &str) -> Option { + let mut parenthes = 0usize; // () + let mut curly_bracket = 0usize; // {} + let mut bracket = 0usize; // [] + let mut angle = 0usize; // <> + + for (i, ch) in link.chars().enumerate() { + match ch { + '(' => { + link_correct!(parenthes, ')', i, link); + } + '{' => { + link_correct!(curly_bracket, '}', i, link); + } + '[' => { + link_correct!(bracket, ']', i, link); + } + '<' => { + link_correct!(angle, '>', i, link); + } + ')' => { + link_correct!(parenthes, i); + } + ']' => { + link_correct!(bracket, i); + } + '}' => { + link_correct!(curly_bracket, i); + } + '>' => { + link_correct!(angle, i); + } + _ => continue, + } + } + None +} + +// IRI links per RFC3987 and RFC3986 +fn parse_iri(input: &str) -> IResult<&str, LinkDestination, CustomError<&str>> { + let input_ = <&str>::clone(&input); + // a link is :// [ipath] [iquery] [ifragment] + let (input, scheme) = scheme(input)?; + // ^ parse scheme + let (input, _period_double_slash) = tag("://")(input)?; + // ^ hey do I need to explain this, too? + let (input, (authority, mut host, is_ipv6_or_future)) = iauthority(input)?; + // host is actually part of authority but we need it separately + // see iauthority function description for more information + let (input, path) = opt(alt(( + recognize(tuple(( + char('/'), + opt(tuple(( + take_while_ipchar1, + many0(tuple((char('/'), opt(take_while_ipchar1)))), + ))), + ))), // ipath-absolute + recognize(tuple(( + take_while_ipchar, + many0(tuple((char('/'), opt(take_while_ipchar1)))), + ))), // ipath-rootless + )))(input)?; + // ^ parse one of ipath-absolute or ipath-rootless or none + // which in the third case it's down to ipath-empty(see below) + let path = path.unwrap_or(""); // it's ipath-empty + let (input, query) = opt(recognize(tuple((char('?'), iquery))))(input)?; + let (_, fragment) = opt(ifragment)(input)?; + let query = query.unwrap_or(""); // in the case of no iquery + let fragment = fragment.unwrap_or(""); // in the case of no ifragment + let ihier_len = 3usize + .saturating_add(authority.len()) + .saturating_add(host.len()) + .saturating_add(path.len()); + // compute length of authority + host + path + let mut len = scheme + .len() + .saturating_add(ihier_len) + .saturating_add(query.len()) + .saturating_add(fragment.len()); + // compute length of link which is ihier_len + scheme + query + fragment + if let Some(link) = input_.get(0..len) { + if link.ends_with([':', ';', '.', ',']) { + len = len.saturating_sub(1); + if path.is_empty() && query.is_empty() && fragment.is_empty() { + host = input_.slice(scheme.len().saturating_add(3)..input_.len().saturating_sub(1)); + } + } + len = get_correct_link(link).unwrap_or(len); + let link = input_.slice(0..len); + let input = input_.slice(len..); + + return Ok(( + input, + LinkDestination { + target: link, + hostname: if host.is_empty() { None } else { Some(host) }, + punycode: if is_ipv6_or_future { + None + } else { + get_puny_code_warning(link, host) + }, + scheme, + }, + )); + } + Err(nom::Err::Failure(CustomError::NoContent)) +} + +/* +// For future +fn parse_irelative_ref(input: &str) -> IResult<&str, Element, CustomError<&str>> { + todo!() +} +*/ + +// White listed links in this format: scheme:some_char like tel:+989164364485 +fn parse_generic(input: &str) -> IResult<&str, LinkDestination, CustomError<&str>> { + let i = <&str>::clone(&input); + let (input, scheme) = scheme(input)?; + if !is_allowed_generic_scheme(scheme) { + return Err(nom::Err::Error(CustomError::InvalidLink)); + } + let (input, rest) = take_while(is_not_white_space)(input)?; + let len = scheme.len().saturating_add(rest.len()); + if let Some(target) = i.get(0..len) { + return Ok(( + input, + LinkDestination { + scheme, + target, + hostname: None, + punycode: None, + }, + )); + } + Err(nom::Err::Failure(CustomError::NoContent)) +} + +pub(super) fn parse_link(input: &str) -> IResult<&str, LinkDestination, CustomError<&str>> { + alt((parse_generic, parse_iri))(input) +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dc71d0f..d7949b0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,9 +1,9 @@ // mod email; +pub mod link_url; pub mod parse_from_text; +mod utils; -mod link_url; - -pub use link_url::LinkDestination; +pub use crate::parser::link_url::LinkDestination; /// The representation of Elements for the Abstract Syntax Tree #[derive(Debug, PartialEq, Eq, Serialize)] diff --git a/src/parser/parse_from_text/base_parsers.rs b/src/parser/parse_from_text/base_parsers.rs index 9881d36..e5cb491 100644 --- a/src/parser/parse_from_text/base_parsers.rs +++ b/src/parser/parse_from_text/base_parsers.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -///! Base utility parsers, used by both text and markdown parsers +// Base utility parsers, used by both text and markdown parsers use nom::{ bytes::complete::tag, error::{ErrorKind, ParseError}, @@ -8,6 +8,8 @@ use nom::{ IResult, }; +use crate::parser::utils::is_white_space; + #[derive(Debug, PartialEq, Eq)] pub enum CustomError { NoContent, @@ -57,18 +59,6 @@ impl IntoCustomError for Result { } } -pub(crate) fn is_white_space(c: char) -> bool { - matches!(c, '\n' | '\r' | '\t' | ' ') -} - -pub(crate) fn is_not_white_space(c: char) -> bool { - !is_white_space(c) -} - -pub(crate) fn is_white_space_but_not_linebreak(c: char) -> bool { - matches!(c, '\t' | ' ') -} - /// delimited no whitespace start or end pub(crate) fn direct_delimited<'a>( input: &'a str, @@ -97,3 +87,9 @@ impl From for Err> { } } */ +/* +impl From> for nom::Err> { + fn from(input: I, code: ErrorKind) -> nom::Err> { + nom::Err(CustomError::Nom(input, code) + } +}*/ diff --git a/src/parser/parse_from_text/desktop_subset.rs b/src/parser/parse_from_text/desktop_subset.rs index 14fbab4..fe25f38 100644 --- a/src/parser/parse_from_text/desktop_subset.rs +++ b/src/parser/parse_from_text/desktop_subset.rs @@ -1,7 +1,7 @@ //! desktop subset of markdown, becase this way we can already use the punycode detection of this crate //! and also we can keep delimited and labled links in desktop -use super::base_parsers::*; +use super::base_parsers::CustomError; use super::markdown_elements::{delimited_email_address, delimited_link, labeled_link}; use super::text_elements::parse_text_element; use super::Element; diff --git a/src/parser/parse_from_text/find_range.rs b/src/parser/parse_from_text/find_range.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/parser/parse_from_text/find_range.rs @@ -0,0 +1 @@ + diff --git a/src/parser/parse_from_text/hashtag_content_char_ranges.rs b/src/parser/parse_from_text/hashtag_content_char_ranges.rs index 7d9ea19..0d816ec 100644 --- a/src/parser/parse_from_text/hashtag_content_char_ranges.rs +++ b/src/parser/parse_from_text/hashtag_content_char_ranges.rs @@ -1,3 +1,4 @@ +use crate::parser::utils::is_in_one_of_ranges; use std::ops::RangeInclusive; const NUMBER_OF_RANGES: usize = 850; @@ -869,72 +870,45 @@ const HASHTAG_CONTENT_CHAR_RANGES: [RangeInclusive; NUMBER_OF_RANGES] = [ 0xe0100..=0xe01ef, ]; -#[derive(Debug, PartialEq, Eq)] -enum FindRangeResult<'a> { - WasOnRangeStart, - Range(&'a RangeInclusive), -} - -fn find_range_for_char<'a>(code: u32) -> FindRangeResult<'a> { - let index = HASHTAG_CONTENT_CHAR_RANGES.binary_search_by_key(&code, |range| *range.start()); - match index { - Ok(_) => FindRangeResult::WasOnRangeStart, - Err(index) => match index { - 0 => FindRangeResult::Range(&HASHTAG_CONTENT_CHAR_RANGES[0]), - // Since `index` can never be 0, `index - 1` will never overflow. Furthermore, the - // maximum value which the binary search function returns is `NUMBER_OF_RANGES`. - // Therefore, `index - 1` will never panic if we index the array with it. - #[allow(clippy::integer_arithmetic, clippy::indexing_slicing)] - index => FindRangeResult::Range(&HASHTAG_CONTENT_CHAR_RANGES[index - 1]), - }, - } -} - pub(crate) fn hashtag_content_char(c: char) -> bool { if matches!(c, '#' | '﹟' | '#' | ' ') { false } else if matches!(c, '+' | '-' | '_') { true } else { - let code: u32 = c as u32; - match find_range_for_char(code) { - FindRangeResult::WasOnRangeStart => true, - FindRangeResult::Range(range) => range.contains(&code), - } + is_in_one_of_ranges(c as u32, &HASHTAG_CONTENT_CHAR_RANGES[..]) } } #[cfg(test)] mod test { use crate::parser::parse_from_text::hashtag_content_char_ranges::hashtag_content_char; - - use super::{find_range_for_char, FindRangeResult, RangeInclusive}; + use crate::parser::utils::is_in_one_of_ranges; + use std::ops::RangeInclusive; #[test] fn test_range_function() { - // these must return WasOnRangeStart - let codes: Vec = vec![0x30000, 0xe0100, 0x23, 0x30, 0x171f, 0x176e, 0x10fb0]; - for code in codes.iter() { - assert_eq!(find_range_for_char(*code), FindRangeResult::WasOnRangeStart); - } - - // these must be return associated ranges - let codes: Vec<(u32, RangeInclusive)> = vec![ - (0x11066 + 5, 0x11066..=0x11075), // in range - (0x11000 + 10, 0x11000..=0x11046), // in range - (0x11046 + 2, 0x11000..=0x11046), // out of range - (0x10, 0x23..=0x23), - (0x09, 0x23..=0x23), - (0x0, 0x23..=0x23), - (0x25, 0x23..=0x23), - (0x2a + 1, 0x2a..=0x2a), - (0xfffff, 0xe0100..=0xe01ef), - // ^ this is beyond ranges and must return the - // last range + let ranges: [RangeInclusive; 5] = [ + 0x0..=0x30, + 0x99..=0x99, + 0x1f..=0x2f, + 0xff..=0xff, + 0x1000f..=0x20000, ]; - - for (code, range) in codes.iter() { - assert_eq!(find_range_for_char(*code), FindRangeResult::Range(range)); + let codes: Vec<(u32, bool)> = vec![ + (0x30000, false), + (0x01, true), + (0x23, true), + (0x30, false), + (0x171f, false), + (0x176e, false), + (0x10fb0, true), + (0x0, true), + (0xf1, false), + ]; + for (code, result) in codes.iter() { + assert_eq!(is_in_one_of_ranges(*code, &ranges[..]), *result); + println!("{code}, {result}"); } } diff --git a/src/parser/parse_from_text/markdown_elements.rs b/src/parser/parse_from_text/markdown_elements.rs index 35a9a88..a239839 100644 --- a/src/parser/parse_from_text/markdown_elements.rs +++ b/src/parser/parse_from_text/markdown_elements.rs @@ -1,18 +1,22 @@ -use crate::parser::link_url::LinkDestination; -use crate::parser::parse_from_text::text_elements::email_address; - -use super::text_elements::{link, parse_text_element}; -use super::Element; -use super::{base_parsers::*, parse_all}; -///! nom parsers for markdown elements use nom::{ bytes::complete::{is_not, tag, take, take_while}, character::complete::alphanumeric1, combinator::{opt, peek, recognize}, - sequence::delimited, + sequence::{delimited, tuple}, IResult, }; +use super::{base_parsers::*, parse_all}; +use crate::parser::{ + link_url::LinkDestination, + parse_from_text::{ + base_parsers::direct_delimited, + text_elements::{email_address, parse_text_element}, + Element, + }, + utils::{is_white_space, is_white_space_but_not_linebreak}, +}; + fn inline_code(input: &str) -> IResult<&str, &str, CustomError<&str>> { delimited(tag("`"), is_not("`"), tag("`"))(input) } @@ -90,15 +94,9 @@ pub(crate) fn delimited_email_address(input: &str) -> IResult<&str, Element, Cus // pub(crate) fn delimited_link(input: &str) -> IResult<&str, Element, CustomError<&str>> { - let (input, content): (&str, &str) = delimited(tag("<"), is_not(">"), tag(">"))(input)?; - if content.is_empty() { - return Err(nom::Err::Error(CustomError::NoContent)); - } - let (rest, link) = link(content)?; - if !rest.is_empty() { - return Err(nom::Err::Error(CustomError::UnexpectedContent)); - } - Ok((input, link)) + let (input, (_, destination, _)): (&str, (&str, LinkDestination, &str)) = + tuple((tag("<"), LinkDestination::parse_labelled, tag(">")))(input)?; + Ok((input, Element::Link { destination })) } // [labeled](https://link) @@ -109,18 +107,10 @@ pub(crate) fn labeled_link(input: &str) -> IResult<&str, Element, CustomError<&s } let label = parse_all(raw_label); - let (input, raw_link): (&str, &str) = delimited(tag("("), is_not(")"), tag(")"))(input)?; - if raw_link.is_empty() { - return Err(nom::Err::Error(CustomError::NoContent)); - } - // check if result is valid link - let (remainder, destination) = LinkDestination::parse(raw_link)?; + let (input, (_, destination, _)) = + tuple((tag("("), LinkDestination::parse_labelled, tag(")")))(input)?; - if remainder.is_empty() { - Ok((input, Element::LabeledLink { label, destination })) - } else { - Err(nom::Err::Error(CustomError::InvalidLink)) - } + Ok((input, Element::LabeledLink { label, destination })) } pub(crate) fn parse_element( diff --git a/src/parser/parse_from_text/mod.rs b/src/parser/parse_from_text/mod.rs index a3180f4..4796b1d 100644 --- a/src/parser/parse_from_text/mod.rs +++ b/src/parser/parse_from_text/mod.rs @@ -2,6 +2,7 @@ use super::Element; pub(crate) mod base_parsers; mod desktop_subset; +pub mod find_range; pub mod hashtag_content_char_ranges; mod markdown_elements; mod text_elements; diff --git a/src/parser/parse_from_text/text_elements.rs b/src/parser/parse_from_text/text_elements.rs index 6914cd6..aeb222f 100644 --- a/src/parser/parse_from_text/text_elements.rs +++ b/src/parser/parse_from_text/text_elements.rs @@ -1,23 +1,20 @@ -///! nom parsers for text elements -use crate::parser::link_url::LinkDestination; - -use super::base_parsers::*; -use super::hashtag_content_char_ranges::hashtag_content_char; -use super::Element; -use crate::nom::{Offset, Slice}; -use nom::bytes::complete::take_while; -use nom::character::complete::char; use nom::{ bytes::{ - complete::{tag, take, take_while1}, + complete::{tag, take, take_while, take_while1}, streaming::take_till1, }, character, + character::complete::char, combinator::{peek, recognize, verify}, sequence::tuple, - AsChar, IResult, + AsChar, IResult, Offset, Slice, }; +use super::base_parsers::CustomError; +use super::hashtag_content_char_ranges::hashtag_content_char; +use super::Element; +use crate::parser::link_url::LinkDestination; + fn linebreak(input: &str) -> IResult<&str, char, CustomError<&str>> { char('\n')(input) } @@ -94,10 +91,15 @@ pub(crate) fn email_address(input: &str) -> IResult<&str, Element, CustomError<& } } +/* fn not_link_part_char(c: char) -> bool { !matches!(c, ':' | '\n' | '\r' | '\t' | ' ') } +fn link(input: &str) -> IResult<&str, (), CustomError<&str>> { + let (input, _) = take_while1(link_scheme)(input)?; +} + /// rough recognition of an link, results gets checked by a real link parser fn link_intern(input: &str) -> IResult<&str, (), CustomError<&str>> { let (input, _) = take_while1(not_link_part_char)(input)?; @@ -225,7 +227,7 @@ pub(crate) fn link(input: &str) -> IResult<&str, Element, CustomError<&str>> { Err(nom::Err::Error(CustomError::InvalidLink)) } } - +*/ fn is_allowed_bot_cmd_suggestion_char(char: char) -> bool { match char { '@' | '\\' | '_' | '/' | '.' | '-' => true, @@ -273,8 +275,8 @@ pub(crate) fn parse_text_element( Ok((i, elm)) } else if let Ok((i, elm)) = email_address(input) { Ok((i, elm)) - } else if let Ok((i, elm)) = link(input) { - Ok((i, elm)) + } else if let Ok((i, destination)) = LinkDestination::parse(input) { + Ok((i, Element::Link { destination })) } else if let Ok((i, _)) = linebreak(input) { Ok((i, Element::Linebreak)) } else { diff --git a/src/parser/utils.rs b/src/parser/utils.rs new file mode 100644 index 0000000..aacbe92 --- /dev/null +++ b/src/parser/utils.rs @@ -0,0 +1,89 @@ +use std::ops::RangeInclusive; + +#[derive(Debug, PartialEq, Eq)] +enum FindRangeResult<'a> { + WasOnRangeStart, + Range(&'a RangeInclusive), +} + +/// Find a range which `code` might be in it. +/// +/// # Description +/// This function gets a sorted slice of inclusive u32 ranges, performs +/// binary search on them and returns a FindRangeResult enum telling +/// which range the `code` might be in. It returns `FindRangeResult::WasOnRangeStart` +/// if the code was exactly on start of a range. Or a `FindRangeResult::Range(range)` +/// which indicates `code` is in `range` or in no ranges. +/// +/// # Arguments +/// +/// - `code` the u32 to look for a range for. +/// +/// - `ranges` a refernce to a slice of `RangeInclusive` +fn find_range_for_char(code: u32, ranges: &'_ [RangeInclusive]) -> FindRangeResult<'_> { + let index = ranges.binary_search_by_key(&code, |range| *range.start()); + match index { + Ok(_) => FindRangeResult::WasOnRangeStart, + Err(index) => match index { + #[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] + 0 => FindRangeResult::Range(&ranges[0]), + // Since `index` can never be 0, `index - 1` will never overflow. Furthermore, the + // maximum value which the binary search function returns is `NUMBER_OF_RANGES`. + // Therefore, `index - 1` will never panic if we index the array with it. + #[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] + index => FindRangeResult::Range(&ranges[index - 1]), + }, + } +} + +/// Returns true of `c` is one of the `ranges`, false otherwise. +/// +/// # Arguments +/// +/// - `c` A number(u32) +/// +/// - `ranges` A sorted slice of ranges to see if `c` is in anyone of them +pub fn is_in_one_of_ranges(c: u32, ranges: &[RangeInclusive]) -> bool { + match find_range_for_char(c, ranges) { + FindRangeResult::WasOnRangeStart => true, + FindRangeResult::Range(range) => range.contains(&c), + } +} + +#[inline(always)] +pub(crate) fn is_alpha(c: char) -> bool { + c.is_alphabetic() +} + +#[inline(always)] +pub(crate) fn is_hex_digit(c: char) -> bool { + c.is_ascii_hexdigit() +} + +#[inline(always)] +pub(crate) fn is_digit(c: char) -> bool { + c.is_ascii_digit() +} + +pub(crate) fn is_sub_delim(c: char) -> bool { + matches!( + c, + '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' + ) +} + +pub(crate) fn is_unreserved(c: char) -> bool { + is_alpha(c) || is_digit(c) || matches!(c, '_' | '.' | '-' | '~') +} + +pub(crate) fn is_white_space(c: char) -> bool { + matches!(c, '\n' | '\r' | '\t' | ' ') +} + +pub(crate) fn is_not_white_space(c: char) -> bool { + !is_white_space(c) +} + +pub(crate) fn is_white_space_but_not_linebreak(c: char) -> bool { + matches!(c, '\t' | ' ') +} diff --git a/tests/links.rs b/tests/links.rs new file mode 100644 index 0000000..a743ae5 --- /dev/null +++ b/tests/links.rs @@ -0,0 +1,150 @@ +#![allow(clippy::unwrap_used)] +use deltachat_message_parser::parser::{link_url::PunycodeWarning, LinkDestination}; + +#[test] +fn basic_parsing() { + let test_cases_no_puny = vec![ + "http://delta.chat", + "http://delta.chat:8080", + "http://localhost", + "http://127.0.0.0", + "https://[::1]/", + "https://[::1]:9000?hi#o", + "https://delta.chat", + "ftp://delta.chat", + "https://delta.chat/en/help", + "https://delta.chat/en/help?hi=5&e=4", + "https://delta.chat?hi=5&e=4", + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "https://delta#section2.0", + "http://delta.chat:8080?hi=5&e=4#section2.0", + "http://delta.chat:8080#section2.0", + "mailto:delta@example.com", + "mailto:delta@example.com?subject=hi&body=hello%20world", + "mailto:foö@ü.chat", + "ftp://test-test", + ]; + + let test_cases_with_puny = vec!["https://ü.app#help", "http://münchen.de"]; + + for input in &test_cases_no_puny { + let (rest, link_destination) = LinkDestination::parse(input) + .unwrap_or_else(|_| panic!("Cannot parse link: {}", input)); + + assert_eq!(input, &link_destination.target); + assert_eq!(rest.len(), 0); + assert!(link_destination.punycode.is_none()); + } + + for input in &test_cases_with_puny { + let Ok((rest, link_destination)) = LinkDestination::parse(input) else { + panic!("Parsing {} as link failed", input); + }; + + assert!(link_destination.punycode.is_some()); + assert_eq!(rest.len(), 0); + assert_eq!(input, &link_destination.target); + } +} + +#[test] +fn invalid_domains() { + let test_cases = vec![";?:/hi", "##://thing"]; + + for input in &test_cases { + println!("testing {input}"); + assert!(LinkDestination::parse(input).is_err()); + } +} + +#[test] +fn punycode_detection() { + assert_eq!( + LinkDestination::parse("http://münchen.de").unwrap().1, + LinkDestination { + hostname: Some("münchen.de"), + target: "http://münchen.de", + scheme: "http", + punycode: Some(PunycodeWarning { + original_hostname: "münchen.de".to_owned(), + ascii_hostname: "xn--mnchen-3ya.de".to_owned(), + punycode_encoded_url: "http://xn--mnchen-3ya.de".to_owned(), + }), + } + ); + + assert_eq!( + LinkDestination::parse("http://muenchen.de").unwrap().1, + LinkDestination { + hostname: Some("muenchen.de"), + target: "http://muenchen.de", + scheme: "http", + punycode: None, + } + ); +} + +#[test] +fn common_schemes() { + assert_eq!( + LinkDestination::parse("http://delta.chat").unwrap(), + ( + "", + LinkDestination { + hostname: Some("delta.chat"), + target: "http://delta.chat", + scheme: "http", + punycode: None, + } + ) + ); + assert_eq!( + LinkDestination::parse("https://far.chickenkiller.com").unwrap(), + ( + "", + LinkDestination { + hostname: Some("far.chickenkiller.com"), + target: "https://far.chickenkiller.com", + scheme: "https", + punycode: None, + } + ) + ); +} +#[test] +fn generic_schemes() { + assert_eq!( + LinkDestination::parse("mailto:someone@example.com").unwrap(), + ( + "", + LinkDestination { + hostname: None, + scheme: "mailto", + punycode: None, + target: "mailto:someone@example.com" + } + ) + ); + assert_eq!( + LinkDestination::parse("bitcoin:bc1qt3xhfvwmdqvxkk089tllvvtzqs8ts06u3u6qka") + .unwrap() + .1, + LinkDestination { + hostname: None, + scheme: "bitcoin", + target: "bitcoin:bc1qt3xhfvwmdqvxkk089tllvvtzqs8ts06u3u6qka", + punycode: None, + } + ); + assert_eq!( + LinkDestination::parse("geo:37.786971,-122.399677") + .unwrap() + .1, + LinkDestination { + scheme: "geo", + punycode: None, + target: "geo:37.786971,-122.399677", + hostname: None + } + ); +} diff --git a/tests/test.rs b/tests/test.rs index f37c391..2aff94a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1 +1,2 @@ +mod links; mod text_to_ast; diff --git a/tests/text_to_ast/desktop_set.rs b/tests/text_to_ast/desktop_set.rs index 3407f7a..eb1ff84 100644 --- a/tests/text_to_ast/desktop_set.rs +++ b/tests/text_to_ast/desktop_set.rs @@ -170,62 +170,138 @@ fn email_address_example() { ] ); } - #[test] fn link() { - let test_cases_no_puny_code = vec![ - "http://delta.chat", - "http://delta.chat:8080", - "http://localhost", - "http://127.0.0.0", - "https://delta.chat", - "ftp://delta.chat", - "https://delta.chat/en/help", - "https://delta.chat/en/help?hi=5&e=4", - "https://delta.chat?hi=5&e=4", - "https://delta.chat/en/help?hi=5&e=4#section2.0", - "http://delta.chat:8080?hi=5&e=4#section2.0", - "http://delta.chat:8080#section2.0", - "mailto:delta@example.com", - "mailto:delta@example.com?subject=hi&body=hello%20world", + let test_cases_no_puny = vec![ + ( + "http://delta.chat", + http_link_no_puny("http://delta.chat", "delta.chat"), + ), + ( + "http://delta.chat:8080", + http_link_no_puny("http://delta.chat:8080", "delta.chat"), + ), + ( + "http://localhost", + http_link_no_puny("http://localhost", "localhost"), + ), + ( + "http://127.0.0.1", + http_link_no_puny("http://127.0.0.1", "127.0.0.1"), + ), + ( + "https://delta.chat", + https_link_no_puny("https://delta.chat", "delta.chat"), + ), + ( + "ftp://delta.chat", + ftp_link_no_puny("ftp://delta.chat", "delta.chat"), + ), + ( + "https://delta.chat/en/help", + https_link_no_puny("https://delta.chat/en/help", "delta.chat"), + ), + ( + "https://delta.chat?hi=5&e=4", + https_link_no_puny("https://delta.chat?hi=5&e=4", "delta.chat"), + ), + ( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", + ), + ), + ( + "https://delta#section2.0", + https_link_no_puny("https://delta#section2.0", "delta"), + ), + ( + "http://delta.chat:8080?hi=5&e=4#section2.0", + http_link_no_puny("http://delta.chat:8080?hi=5&e=4#section2.0", "delta.chat"), + ), + ( + "http://delta.chat:8080#section2.0", + http_link_no_puny("http://delta.chat:8080#section2.0", "delta.chat"), + ), + ( + "mailto:delta@example.com", + mailto_link_no_puny("mailto:delta@example.com"), + ), + ( + "mailto:delta@example.com?subject=hi&body=hello%20world", + mailto_link_no_puny("mailto:delta@example.com?subject=hi&body=hello%20world"), + ), + ( + "mailto:foö@ü.chat", + mailto_link_no_puny("mailto:foö@ü.chat"), + ), + ( + "https://delta.chat/%C3%BC%C3%A4%C3%B6", + https_link_no_puny( + "https://delta.chat/%C3%BC%C3%A4%C3%B6", + "delta.chat", + ) + ), + ( + "https://delta.chat/üäö", + https_link_no_puny( + "https://delta.chat/üäö", + "delta.chat", + ) + ), + ( + "https://90eghtesadi.com/Keywords/Index/2031708/%D9%82%D8%B1%D8%A7%D8%B1%D8%AF%D8%A7%D8%AF-%DB%B2%DB%B5-%D8%B3%D8%A7%D9%84%D9%87-%D8%A7%DB%8C%D8%B1%D8%A7%D9%86-%D9%88-%DA%86%DB%8C%D9%86", + // ^ I guess shame on the Iranian government of the time? --Farooq + https_link_no_puny( + "https://90eghtesadi.com/Keywords/Index/2031708/%D9%82%D8%B1%D8%A7%D8%B1%D8%AF%D8%A7%D8%AF-%DB%B2%DB%B5-%D8%B3%D8%A7%D9%84%D9%87-%D8%A7%DB%8C%D8%B1%D8%A7%D9%86-%D9%88-%DA%86%DB%8C%D9%86", + "90eghtesadi.com", + ) + ), + ( + "https://pcworms.ir/صفحه", + https_link_no_puny( + "https://pcworms.ir/صفحه", + "pcworms.ir", + ), + ), + ( + "gopher://republic.circumlunar.space/1/~farooqkz", + gopher_link_no_puny( + "gopher://republic.circumlunar.space/1/~farooqkz", + "republic.circumlunar.space", + ), + ), ]; - let test_cases_with_punycode = vec![ - "mailto:foö@ü.chat", + + let test_cases_with_puny = [( "https://ü.app#help", - "https://delta#section2.0", - ]; + https_link_no_puny("https://ü.app#help", "ü.app"), + )]; - for input in &test_cases_no_puny_code { + for (input, destination) in &test_cases_no_puny { println!("testing {input}"); assert_eq!( parse_desktop_set(input), vec![Link { - destination: link_destination_for_testing(input) + destination: destination.clone() }] ); - let result = parse_desktop_set(input); - assert_eq!(result.len(), 1); - assert!(matches!( - result[0], - Link { - destination: LinkDestination { - target: _, - punycode: None, - hostname: _, - scheme: _, - } - } - )); } - for input in &test_cases_with_punycode { + for (input, expected_destination) in &test_cases_with_puny { println!("testing {input}"); - assert_eq!( - parse_desktop_set(input), - vec![Link { - destination: link_destination_for_testing(input), - }] - ); + match &parse_desktop_set(input)[0] { + Link { destination } => { + assert_eq!(expected_destination.target, destination.target); + assert_eq!(expected_destination.scheme, destination.scheme); + assert_eq!(expected_destination.hostname, destination.hostname); + assert!(destination.punycode.is_some()); + } + _ => { + panic!(); + } + } } } @@ -238,8 +314,9 @@ fn test_link_example() { vec![ Text("This is an my site: "), Link { - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat" ) }, Linebreak, @@ -267,8 +344,9 @@ fn labeled_link_should_not_work() { parse_desktop_set("[a link](https://delta.chat/en/help?hi=5&e=4#section2.0)"), vec![LabeledLink { label: vec![Text("a link")], - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", ) }] ); @@ -278,8 +356,9 @@ fn labeled_link_should_not_work() { ), vec![LabeledLink { label: vec![Text("rich content "), Bold(vec![Text("bold")])], - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", ) }] ); @@ -293,7 +372,7 @@ fn labeled_link_example_should_not_work() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat") }, Text(".") ] @@ -306,7 +385,7 @@ fn inline_link_do_not_eat_last_char_if_it_is_special() { parse_desktop_set("https://delta.chat,"), vec![ Link { - destination: link_destination_for_testing("https://delta.chat") + destination: https_link_no_puny("https://delta.chat", "delta.chat") }, Text(",") ] @@ -315,7 +394,7 @@ fn inline_link_do_not_eat_last_char_if_it_is_special() { parse_desktop_set("https://delta.chat."), vec![ Link { - destination: link_destination_for_testing("https://delta.chat") + destination: https_link_no_puny("https://delta.chat", "delta.chat") }, Text(".") ] @@ -323,7 +402,7 @@ fn inline_link_do_not_eat_last_char_if_it_is_special() { assert_eq!( parse_desktop_set("https://delta.chat/page.hi"), vec![Link { - destination: link_destination_for_testing("https://delta.chat/page.hi") + destination: https_link_no_puny("https://delta.chat/page.hi", "delta.chat") }] ); } diff --git a/tests/text_to_ast/links.rs b/tests/text_to_ast/links.rs new file mode 100644 index 0000000..38b3656 --- /dev/null +++ b/tests/text_to_ast/links.rs @@ -0,0 +1,26 @@ +use super::*; +use deltachat_message_parser::parser::{parse_link, LinkDestination}; + +#[test] +fn link() { + let test_cases = vec![ + "http://delta.chat", + "http://delta.chat:8080", + "http://localhost", + "http://127.0.0.0", + "https://delta.chat", + "ftp://delta.chat", + "https://delta.chat/en/help", + "https://delta.chat/en/help?hi=5&e=4", + "https://delta.chat?hi=5&e=4", + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "https://delta#section2.0", + "http://delta.chat:8080?hi=5&e=4#section2.0", + "http://delta.chat:8080#section2.0", + "mailto:delta@example.com", + "mailto:delta@example.com?subject=hi&body=hello%20world", + "mailto:foö@ü.chat", + "https://ü.app#help", // TODO add more urls for testing + ]; + +} diff --git a/tests/text_to_ast/markdown.rs b/tests/text_to_ast/markdown.rs index d3dc0e3..fe19090 100644 --- a/tests/text_to_ast/markdown.rs +++ b/tests/text_to_ast/markdown.rs @@ -493,44 +493,120 @@ fn email_address_example() { #[test] fn link() { - let test_cases = vec![ - "http://delta.chat", - "http://delta.chat:8080", - "http://localhost", - "http://127.0.0.0", - "https://delta.chat", - "ftp://delta.chat", - "https://delta.chat/en/help", - "https://delta.chat/en/help?hi=5&e=4", - "https://delta.chat?hi=5&e=4", - "https://delta.chat/en/help?hi=5&e=4#section2.0", - "https://delta#section2.0", - "http://delta.chat:8080?hi=5&e=4#section2.0", - "http://delta.chat:8080#section2.0", - "mailto:delta@example.com", - "mailto:delta@example.com?subject=hi&body=hello%20world", - "mailto:foö@ü.chat", - "https://ü.app#help", // TODO add more url test cases + let test_cases_no_puny = vec![ + ( + "http://delta.chat", + http_link_no_puny("http://delta.chat", "delta.chat"), + ), + ( + "http://delta.chat:8080", + http_link_no_puny("http://delta.chat:8080", "delta.chat"), + ), + ( + "http://localhost", + http_link_no_puny("http://localhost", "localhost"), + ), + ( + "http://127.0.0.1", + http_link_no_puny("http://127.0.0.1", "127.0.0.1"), + ), + ( + "https://delta.chat", + https_link_no_puny("https://delta.chat", "delta.chat"), + ), + ( + "ftp://delta.chat", + ftp_link_no_puny("ftp://delta.chat", "delta.chat"), + ), + ( + "https://delta.chat/en/help", + https_link_no_puny("https://delta.chat/en/help", "delta.chat"), + ), + ( + "https://delta.chat?hi=5&e=4", + https_link_no_puny("https://delta.chat?hi=5&e=4", "delta.chat"), + ), + ( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", + ), + ), + ( + "https://delta#section2.0", + https_link_no_puny("https://delta#section2.0", "delta"), + ), + ( + "http://delta.chat:8080?hi=5&e=4#section2.0", + http_link_no_puny("http://delta.chat:8080?hi=5&e=4#section2.0", "delta.chat"), + ), + ( + "http://delta.chat:8080#section2.0", + http_link_no_puny("http://delta.chat:8080#section2.0", "delta.chat"), + ), + ( + "mailto:delta@example.com", + mailto_link_no_puny("mailto:delta@example.com"), + ), + ( + "mailto:delta@example.com?subject=hi&body=hello%20world", + mailto_link_no_puny("mailto:delta@example.com?subject=hi&body=hello%20world"), + ), + ( + "mailto:foö@ü.chat", + mailto_link_no_puny("mailto:foö@ü.chat"), + ), + ( + "gopher://[::1]/", + gopher_link_no_puny("gopher://[::1]/", "[::1]"), + ), + ( + "https://[2345:0425:2CA1:0000:0000:0567:5673:23b5]/hello_world", + https_link_no_puny( + "https://[2345:0425:2CA1:0000:0000:0567:5673:23b5]/hello_world", + "[2345:0425:2CA1:0000:0000:0567:5673:23b5]", + ), + ), + ( + "https://[2345:425:2CA1:0:0:0567:5673:23b5]/hello_world", + https_link_no_puny( + "https://[2345:425:2CA1:0:0:0567:5673:23b5]/hello_world", + "[2345:425:2CA1:0:0:0567:5673:23b5]", + ), + ), ]; - for input in &test_cases { - println!("testing {}", input); + let test_cases_with_puny = [( + "https://ü.app#help", + https_link_no_puny("https://ü.app#help", "ü.app"), + )]; + + for (input, expected_destination) in &test_cases_no_puny { + println!("testing {input}"); + let result = parse_markdown_text(input); + assert_eq!(result.len(), 1); assert_eq!( - parse_markdown_text(input), - vec![Link { - destination: link_destination_for_testing(input) - }] + result[0], + Link { + destination: expected_destination.clone() + } ); } - for input in &test_cases { - println!("testing <{}>", input); - assert_eq!( - parse_markdown_text(input), - vec![Link { - destination: link_destination_for_testing(input) - }] - ); + for (input, expected_destination) in &test_cases_with_puny { + println!("testing {}", input); + match &parse_markdown_text(input)[0] { + Link { destination } => { + assert_eq!(expected_destination.target, destination.target); + assert_eq!(expected_destination.scheme, destination.scheme); + assert_eq!(expected_destination.hostname, destination.hostname,); + assert!(destination.punycode.is_some()); + } + _ => { + panic!(); + } + } } } @@ -543,8 +619,9 @@ fn test_link_example() { vec![ Text("This is an my site: "), Link { - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat" ) }, Linebreak, @@ -575,8 +652,9 @@ fn test_delimited_link_example() { vec![ Text("This is an my site: "), Link { - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat" ) }, Linebreak, @@ -591,9 +669,10 @@ fn labeled_link() { parse_markdown_text("[a link](https://delta.chat/en/help?hi=5&e=4#section2.0)"), vec![LabeledLink { label: vec![Text("a link")], - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" - ) + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat" + ), }] ); assert_eq!( @@ -602,9 +681,21 @@ fn labeled_link() { ), vec![LabeledLink { label: vec![Text("rich content "), Bold(vec![Text("bold")])], - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" - ) + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat" + ), + }] + ); +} + +#[test] +fn labeled_link_parenthesis_in_target() { + assert_eq!( + parse_markdown_text("[a link](https://delta.chat/en/help(help)hi)"), + vec![LabeledLink { + label: vec![Text("a link")], + destination: https_link_no_puny("https://delta.chat/en/help(help)hi", "delta.chat"), }] ); } @@ -617,7 +708,7 @@ fn labeled_link_example() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat"), }, Text(".") ] @@ -632,7 +723,7 @@ fn labeled_link_can_have_comma_or_dot_at_end() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help.") + destination: https_link_no_puny("https://delta.chat/en/help.", "delta.chat"), }, Text(".") ] @@ -643,7 +734,7 @@ fn labeled_link_can_have_comma_or_dot_at_end() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help,") + destination: https_link_no_puny("https://delta.chat/en/help,", "delta.chat"), }, Text(".") ] @@ -654,7 +745,7 @@ fn labeled_link_can_have_comma_or_dot_at_end() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help:") + destination: https_link_no_puny("https://delta.chat/en/help:", "delta.chat"), }, Text(".") ] @@ -665,7 +756,7 @@ fn labeled_link_can_have_comma_or_dot_at_end() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help;") + destination: https_link_no_puny("https://delta.chat/en/help;", "delta.chat"), }, Text(".") ] diff --git a/tests/text_to_ast/mod.rs b/tests/text_to_ast/mod.rs index af385cb..0d2c471 100644 --- a/tests/text_to_ast/mod.rs +++ b/tests/text_to_ast/mod.rs @@ -1,8 +1,49 @@ use deltachat_message_parser::parser::Element::*; use deltachat_message_parser::parser::LinkDestination; -pub fn link_destination_for_testing(trusted_real_url: &str) -> LinkDestination { - LinkDestination::parse(trusted_real_url).unwrap().1 +fn gopher_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "gopher", + punycode: None, + } +} + +fn http_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "http", + punycode: None, + } +} + +fn ftp_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "ftp", + punycode: None, + } +} + +fn https_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "https", + punycode: None, + } +} + +fn mailto_link_no_puny(target: &str) -> LinkDestination<'_> { + LinkDestination { + target, + hostname: None, + scheme: "mailto", + punycode: None, + } } mod desktop_set; diff --git a/tests/text_to_ast/mod.rs.orig b/tests/text_to_ast/mod.rs.orig new file mode 100644 index 0000000..a1a809d --- /dev/null +++ b/tests/text_to_ast/mod.rs.orig @@ -0,0 +1,63 @@ +use deltachat_message_parser::parser::Element::*; +use deltachat_message_parser::parser::LinkDestination; + +fn gopher_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "gopher", + punycode: None, + } +} + +<<<<<<< HEAD +======= +fn internal_link(target: &str) -> LinkDestination<'_> { + LinkDestination { + target, + hostname: None, + scheme: "", + punycode: None, + } +} + +>>>>>>> a0203f4363e504cbe5d32a846a9c8770d6442cf7 +fn http_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "http", + punycode: None, + } +} + +fn ftp_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "ftp", + punycode: None, + } +} + +fn https_link_no_puny<'a>(target: &'a str, hostname: &'a str) -> LinkDestination<'a> { + LinkDestination { + target, + hostname: Some(hostname), + scheme: "https", + punycode: None, + } +} + +fn mailto_link_no_puny(target: &str) -> LinkDestination<'_> { + LinkDestination { + target, + hostname: None, + scheme: "mailto", + punycode: None, + } +} + +mod desktop_set; +mod markdown; +mod text_only; diff --git a/tests/text_to_ast/text_only.rs b/tests/text_to_ast/text_only.rs index ee03e30..0ba85a2 100644 --- a/tests/text_to_ast/text_only.rs +++ b/tests/text_to_ast/text_only.rs @@ -1,5 +1,5 @@ use super::*; -use deltachat_message_parser::parser::{parse_only_text, LinkDestination}; +use deltachat_message_parser::parser::parse_only_text; #[test] fn do_not_parse_markdown_elements() { @@ -271,63 +271,6 @@ fn email_address_do_not_parse_last_char_if_special() { ); } -#[test] -fn link() { - let test_cases = vec![ - "http://delta.chat", - "http://delta.chat:8080", - "http://localhost", - "http://127.0.0.0", - "https://delta.chat", - "ftp://delta.chat", - "https://delta.chat/en/help", - "https://delta.chat/en/help?hi=5&e=4", - "https://delta.chat?hi=5&e=4", - "https://delta.chat/en/help?hi=5&e=4#section2.0", - "https://delta#section2.0", - "http://delta.chat:8080?hi=5&e=4#section2.0", - "http://delta.chat:8080#section2.0", - "mailto:delta@example.com", - "mailto:delta@example.com?subject=hi&body=hello%20world", - "mailto:foö@ü.chat", - "https://ü.app#help", // TODO add more urls for testing - ]; - - for input in &test_cases { - println!("testing {}", input); - assert_eq!( - parse_only_text(input), - vec![Link { - destination: link_destination_for_testing(input) - }] - ); - } - - for input in &test_cases { - println!("testing <{}>", input); - assert_eq!( - parse_only_text(input), - vec![Link { - destination: link_destination_for_testing(input) - }] - ); - } - - let input = "http://[2001:0db8:85a3:08d3::0370:7344]:8080/"; - let hostname = "[2001:0db8:85a3:08d3::0370:7344]"; - assert_eq!( - parse_only_text(input), - vec![Link { - destination: LinkDestination { - target: input, - hostname: Some(hostname), - punycode: None, - scheme: "http" - } - }] - ); -} - #[test] fn test_link_example() { assert_eq!( @@ -337,8 +280,9 @@ fn test_link_example() { vec![ Text("This is an my site: "), Link { - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", ) }, Linebreak, @@ -352,7 +296,7 @@ fn delimited_email_should_not_work() { assert_ne!( parse_only_text("This is an my site: \nMessage me there"), vec![ - Text("This is an my site: "), + Text("This is an my email: "), EmailAddress("hello@delta.chat"), Linebreak, Text("Message me there") @@ -369,8 +313,9 @@ fn delimited_link_should_not_work() { vec![ Text("This is an my site: "), Link { - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", ) }, Linebreak, @@ -385,8 +330,9 @@ fn labeled_link_should_not_work() { parse_only_text("[a link](https://delta.chat/en/help?hi=5&e=4#section2.0)"), vec![LabeledLink { label: vec![Text("a link")], - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", ) }] ); @@ -394,8 +340,9 @@ fn labeled_link_should_not_work() { parse_only_text("[rich content **bold**](https://delta.chat/en/help?hi=5&e=4#section2.0)"), vec![LabeledLink { label: vec![Text("rich content "), Bold(vec![Text("bold")])], - destination: link_destination_for_testing( - "https://delta.chat/en/help?hi=5&e=4#section2.0" + destination: https_link_no_puny( + "https://delta.chat/en/help?hi=5&e=4#section2.0", + "delta.chat", ) }] ); @@ -409,7 +356,7 @@ fn labeled_link_example_should_not_work() { Text("you can find the details "), LabeledLink { label: vec![Text("here")], - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat") }, Text(".") ] @@ -423,7 +370,7 @@ fn link_do_not_consume_last_comma() { vec![ Text("you can find the details on "), Link { - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat") }, Text(",") ] @@ -437,7 +384,7 @@ fn link_do_not_consume_last_semicolon_or_colon() { vec![ Text("you can find the details on "), Link { - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat") }, Text(";") ] @@ -447,7 +394,7 @@ fn link_do_not_consume_last_semicolon_or_colon() { vec![ Text("you can find the details on "), Link { - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat") }, Text(":") ] @@ -461,7 +408,7 @@ fn link_do_not_consume_last_dot() { vec![ Text("you can find the details on "), Link { - destination: link_destination_for_testing("https://delta.chat/en/help") + destination: https_link_no_puny("https://delta.chat/en/help", "delta.chat") }, Text(".") ] @@ -471,7 +418,7 @@ fn link_do_not_consume_last_dot() { vec![ Text("you can find the details on "), Link { - destination: link_destination_for_testing("https://delta.chat/en/help.txt") + destination: https_link_no_puny("https://delta.chat/en/help.txt", "delta.chat") }, Text(".") ] @@ -485,7 +432,7 @@ fn link_with_file_extention() { vec![ Text("you can find the details on "), Link { - destination: link_destination_for_testing("https://delta.chat/en/help.html") + destination: https_link_no_puny("https://delta.chat/en/help.html", "delta.chat") } ] ); @@ -498,7 +445,7 @@ fn parenthesis_in_links() { vec![ Text("links can contain parenthesis, "), Link { - destination: link_destination_for_testing("https://en.wikipedia.org/wiki/Bracket_(disambiguation)") + destination: https_link_no_puny("https://en.wikipedia.org/wiki/Bracket_(disambiguation)", "en.wikipedia.org") }, Text(" is an example of this.") ] @@ -514,8 +461,9 @@ fn link_in_parenthesis() { vec![ Text("for more information see ("), Link { - destination: link_destination_for_testing( - "https://github.com/deltachat/message-parser/issues/12" + destination: https_link_no_puny( + "https://github.com/deltachat/message-parser/issues/12", + "github.com" ) }, Text(")") @@ -530,7 +478,7 @@ fn link_with_parenthesis_in_parenthesis() { vec![ Text("there are links that contain parenthesis (for example "), Link { - destination: link_destination_for_testing("https://en.wikipedia.org/wiki/Bracket_(disambiguation)") + destination: https_link_no_puny("https://en.wikipedia.org/wiki/Bracket_(disambiguation)", "en.wikipedia.org") }, Text(")") ] @@ -546,39 +494,12 @@ fn link_with_different_parenthesis_in_parenthesis() { vec![ Text("()(for [example{ "), Link { - destination: link_destination_for_testing( - "https://en.wikipedia.org/wiki/Bracket_(disambiguation){[}hi]" + destination: https_link_no_puny( + "https://en.wikipedia.org/wiki/Bracket_(disambiguation)", + "en.wikipedia.org" ) }, - Text("])}") - ] - ); -} - -#[test] -fn link_with_backets_in_backets() { - assert_eq!( - parse_only_text("there are links that contain backets [for example https://en.wikipedia.org/wiki/Bracket_[disambiguation]]"), - vec![ - Text("there are links that contain backets [for example "), - Link { - destination: link_destination_for_testing("https://en.wikipedia.org/wiki/Bracket_[disambiguation]") - }, - Text("]") - ] - ); -} - -#[test] -fn link_with_parenthesis_in_parenthesis_curly() { - assert_eq!( - parse_only_text("there are links that contain parenthesis {for example https://en.wikipedia.org/wiki/Bracket_{disambiguation}}"), - vec![ - Text("there are links that contain parenthesis {for example "), - Link { - destination: link_destination_for_testing("https://en.wikipedia.org/wiki/Bracket_{disambiguation}") - }, - Text("}") + Text("{[}hi]])}") ] ); } @@ -589,7 +510,7 @@ fn link_with_descriptive_parenthesis() { parse_only_text("https://delta.chat/page(this is the link to our site)"), vec![ Link { - destination: link_destination_for_testing("https://delta.chat/page") + destination: https_link_no_puny("https://delta.chat/page", "delta.chat") }, Text("(this is the link to our site)") ] @@ -603,7 +524,7 @@ fn link_in_parenthesis2() { vec![ Text("A great chat app (see "), Link { - destination: link_destination_for_testing("https://delta.chat/en/") + destination: https_link_no_puny("https://delta.chat/en/", "delta.chat") }, Text(")") ]