ijdickinson

ijdickinson

As part of our data validation, I’d like to spot unexpected UTF8 characters in user supplied details. Some non-ASCII characters can be anticipated (é for example), but I’d like to spot unintended changes like UTF-8 single quote mark in place of ASCII ', invisible spaces, etc.

So the question I have is: what’s a good way to scan an Elixir string to detect characters that are in UTF8 but outside the range of ASCII (i.e. which would require File.open/2 to use :utf8 mode when writing)?

Showing Posts 1 to 10

dimitarvp

dimitarvp

You can just make an allow-list of characters (since you said you are not only after ASCII characters but a few more as well), open a file in :utf8 mode and then work on each character via the in operator and the String.codepoints function. Seems easy.

dwark

dwark

In addition to @dimitarvp 's reply, when rolling your own maybe this
String.valid? thread could be helpful.

cevado

cevado

I have this library used to transliterate unicode to ascii… it’s based on the pearl and ruby version of it. from your question it seems to be good enough:

edit:
also if you’re going from utf-8 to a specific encoding. maybe using codepagex might work better:

dwark

dwark

I had a use-case to simply detect non-ascii and used:

name != for(<<c <- name>>, c < 128, into: "", do: <<c>>)

which seems to do the trick. Didn’t need to be fast per se.

kip

kip

ex_cldr Core Team

Is こんにちは unexpected UTF-8? Is नमस्ते? I’m curious what the issue is, in your use case, with valid UTF-8?

I think the answer to that would help decide whether whitelisting or blacklisting or blacklisting is the better choice.

ijdickinson

ijdickinson OP

Which of these is more likely to be correct input from a user:

jane.o’brien@example.com
jane.o’brien@example.com

?

Turns out, one of them is in ascii, and one not. We can tell this, because an IO.write/2 with one of those strings will :boom: unless the output is opened in :utf8: mode.

It’s unlikely (not impossible, but unlikely) that the UTF8 apostrophe is part of a correct email address. However, we can’t just reject UTF8 outright because Anders.Ångström@example.com is entirely legitimate. So the goal is to detect unexpected input that might be a mistake, and flag it for a human reviewer to check. Hence the task: point out characters that are valid UTF8 codepoints, but outside ASCII, allowing for a a list of common exceptions. The exceptions are basically Roman alphabet characters with accents.

Given the current audience for our app, at the moment it’s very unlikely we’ll get a string of entirely Arabic or Kanji or whatever. If that becomes an issue in the future, we’ll have to redesign the approach, but for today yagni.

kip

kip

ex_cldr Core Team

こんにちは@example.com is also entirely legitimate as you know. I empathise with the intent to help check user-supplied input but other than actually sending a validation email you have some risk ending up with as many complaints about blocking valid email addresses as you do fixing unintentional input errors.

You might get some additional ideas from the Unicode Security Guide which overlaps, in part, with your objectives.

dimitarvp

dimitarvp

Confusing part is why would you do IO.write without :utf8 mode. Still, OK:

defmodule AllowlistUnicode do
  def allowset_to_list(list) when is_list(list) do
    list |> allowset_element_to_list() |> List.flatten()
  end

  def allowset_element_to_list([]), do: []

  def allowset_element_to_list([range | rest]) when is_struct(range, Range) do
    [Range.to_list(range) | allowset_element_to_list(rest)]
  end

  def allowset_element_to_list([allowed | rest]) when is_list(allowed) or is_integer(allowed) do
    [allowed | allowset_element_to_list(rest)]
  end

  def get_allowed_and_blocked_characters(string, allow_list)
      when is_binary(string) and is_list(allow_list) do
    allow_list = allowset_to_list(allow_list)

    string
    |> String.to_charlist()
    |> Enum.reduce({[], []}, fn char, {allowed, blocked} ->
      if char in allow_list do
        {[char | allowed], blocked}
      else
        {allowed, [char | blocked]}
      end
    end)
    |> then(fn {allowed, blocked} ->
      {Enum.reverse(allowed), Enum.reverse(blocked)}
    end)
  end
end

This allows you to test with arbitrary mixes of lists, Ranges and separate integers like so:

iex(11)> AllowlistUnicode.get_allowed_and_blocked_characters("гzжf", [0..128, 8217, [0x2D, 0x2011]])
{~c"zf", [1075, 1078]}

I.e. a tuple where the first element is a list of allowed characters and the second one: of the blocked characters.

That way you can easily filter out stuff you dislike.

I’ll agree with @kip that this is a slippery slope and carries the potential of you having to respond to human support requests for a while until you nail your audience… and then indeed any Chinese / Korean / Japanese / Arabic name will trip your code up again. But maybe you’re OK with it, hence the code above.

Or if you only want to check against a pre-constructed allow-list + only need either the allowed or the blocked characters then @dwark’s code is a literal one-liner that gets the job done just fine.

ijdickinson

ijdickinson OP

Well I didn’t expect the Spanish Inquisition. It just happened that way. We’ve had six months of processing bulk customer data, which was all fine until it wasn’t. The IO.write is fixed, obviously, but the interesting part - to me - is that missing the :utf8 flag exposed an error case in the data pipeline that we hadn’t come across before. Production code has bugs sometimes. You find them, you fix them, and move on.

ijdickinson

ijdickinson OP

Thanks for all the comments and suggestions, folks. I have enough now to make our data pipeline a bit more robust (at least to the error cases we know about!)

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews