4
\$\begingroup\$

Problem

A function is needed, that obfuscates text but preserves line breaks. Line breaks may be encoded by "\r\n" or "\r", even mixed. myObfuscator is an object that obfuscates text, ignoring line breaks (would be replaced by any other character).

Example:

ABC\r\nDEF\rGHE

should be obfuscated to something like

XXX\r\nXXX\rXXX

where XXX depends on myObfuscator (not important for the question).

Implementation

My approach works, but seems a little too complicated.

public string Obfuscate(string text)
{
    var stringBuilder = new StringBuilder();
    var lineCount = 0;
    var lines = text.Split(new[] { "\r\n" }, StringSplitOptions.None);
    foreach (var line in lines)
    {
        var innerLineCount = 0;
        if (lineCount > 0) stringBuilder.Append("\r\n");
        foreach (var innerLine in line.Split(new[] { "\r" }, StringSplitOptions.None))
        {
            if (innerLineCount > 0) stringBuilder.Append("\r");
            var obfuscatedLine = string.IsNullOrEmpty(innerLine) ? string.Empty : myObfuscator.Obfuscate(innerLine);
            stringBuilder.Append(obfuscatedLine);
            innerLineCount++;
        }
        lineCount++;
    }
    return stringBuilder.ToString();
}

Is there a more elegant solution?

\$\endgroup\$
3
\$\begingroup\$

This simple text manipulation task is perfect for regular expressions.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text.RegularExpressions;

namespace Tests
{
    public static class StringUtils
    {
        public delegate string Obfuscate(string str);

        public static string ObfuscateLines(string str, Obfuscate obfuscate)
            => new Regex(@"[^\r\n]+").Replace(str, match => obfuscate(match.Value));
    }

    [TestClass]
    public class ObfuscatorTest
    {
        [TestMethod]
        public void TestObfuscate()
        {
            Assert.AreEqual("A\r\nLLLL\rSSSSSSSSS",
                StringUtils.ObfuscateLines(
                    "A\r\nLong\rSentence.",
                    line => new string(line[0], line.Length)));

            Assert.AreEqual("XXX\r\nXXX\rXXX",
                StringUtils.ObfuscateLines(
                    "A\r\nLong\rSentence.",
                    line => "XXX"));
        }
    }
}

I don't know C# very well, therefore I did not manage to do the same code with Obfuscator being an interface, but I'm sure you can find out.

\$\endgroup\$
  • \$\begingroup\$ So nice and short with regex ;-] \$\endgroup\$ – t3chb0t Oct 12 '17 at 3:55
  • \$\begingroup\$ I added a working regex based solution to my answer. :) \$\endgroup\$ – Roland Illig Oct 12 '17 at 17:47
  • \$\begingroup\$ Great solution, thanks! :). That I don't come up with such obvious solutions myself -.-. \$\endgroup\$ – JanDotNet Oct 13 '17 at 8:39
6
\$\begingroup\$

I would omit the StringBuilder here and would go with string.Join() by joining the processed innerLines and the processed lines like so

public string Obfuscate(string text)
{

    var lines = text.Split(new[] { "\r\n" }, StringSplitOptions.None);

    for (int i = 0; i < lines.Length; i++)
    {

        var innerLines = lines[i].Split(new[] { "\r" }, StringSplitOptions.None);
        for (int j = 0; j < innerLines.Length; j++)
        {
            innerLines[j]= string.IsNullOrEmpty(innerLines[j]) ? string.Empty : myObfuscator.Obfuscate(innerLines[j]);
        }

        lines[i] = string.Join("\r", innerLines);

    }
    return string.Join(Environment.NewLine, lines);

}  

What I would add about your code would be some proper argument validation because the method is public. In addition I would move the check wether ther innerLine is NullOrEmpty inside the obfuscator.

\$\endgroup\$
  • \$\begingroup\$ Reusing the arrays and moving the "is string null or empty check" to the obfuscater is a good idea. I will do it that way - thanks :). \$\endgroup\$ – JanDotNet Oct 11 '17 at 15:05
4
\$\begingroup\$

You could use single loop and a buffer that you obfuscate and empty each time you find either a \r or \n. Then you can fix the line breaks and join the strings again:

public static IEnumerable<string> Obfuscate(string text)
{
    var buffer = new StringBuilder();
    foreach (var c in text)
    {
        if(IsNewLine(c) && buffer.Length > 0)
        {
            yield return Obfuscator.Obfuscate(buffer.ToString());
            buffer.Clear();
        }
        else
        {
            buffer.Append(c);
        }
    }

    if (buffer.Length > 0)
    {
        yield return Obfuscator.Obfuscate(buffer.ToString());       
    }

    bool IsNewLine(char c) => c == '\r' || c == '\n';
}

Preserving line beraks is also relitively easy. You just need to change the foreach into a while and use an enumerator that you can pass to the ReadLineBreaks method that will continue reading line break chars. Unlike your method, this one will also consume line breaks at the and of a string.

public static string Obfuscate(string text)
{
    var result = new StringBuilder();
    var buffer = new StringBuilder();
    using (var enumerator = text.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            if (IsNewLine(enumerator.Current) && buffer.Length > 0)
            {
                result.Append(Obfuscator.Obfuscate(buffer.ToString()));
                result.Append(ReadLineBreaks(enumerator, out var moveNext));
                buffer.Clear();
                if (moveNext)
                {
                    buffer.Append(enumerator.Current);
                }
            }
            else
            {
                buffer.Append(enumerator.Current);
            }
        }

        if (buffer.Length > 0)
        {
            result.Append(Obfuscator.Obfuscate(buffer.ToString()));
        }
    }

    return result.ToString();
}

private static string ReadLineBreaks(IEnumerator<char> enumerator, out bool moveNext)
{
    moveNext = false;
    var lineBreaks = new StringBuilder();
    while (IsNewLine(enumerator.Current))
    {
        lineBreaks.Append(enumerator.Current);
        moveNext = enumerator.MoveNext();
        if (!moveNext) break;
    }
    return lineBreaks.ToString();
}

private static bool IsNewLine(char c) => c == '\r' || c == '\n';
\$\endgroup\$
  • \$\begingroup\$ Thanks for the alternative approach with the buffer. Do you think that solution is more readable / elegant than the code in the question? \$\endgroup\$ – JanDotNet Oct 12 '17 at 12:21
2
\$\begingroup\$

If the obfuscation is strictly dependent upon the character being obfuscated (i.e., a always maps to L) and all of the non-obfuscated characters need to remain as-is (i.e., \r\n and \r need to remain the same in the output), the following works:

public string ObfuscateWrapper(string original, Func<char, char> obfuscator, char[] ignoreChars = null) {
    ignoreChars = ignoreChars ?? new char[] { };
    return new String(original.ToCharArray().Select(c => ignoreChars.Contains(c) ? c : obfuscator(c)).ToArray());
}

If you want to normalize the line endings, I'd suggest doing that elsewhere.

\$\endgroup\$
0
\$\begingroup\$

You could just separate out all the "valid" characters, apply obfuscation, and then put them back. In Python this could be implemented as:

import pandas as pd
# Turn the string into a Series of characters
string_series = pd.Series([ch for ch in string_to_be_obfuscated])
# Create a Series of Booleans keeping track of whether 
#    the character at that index is a character you want obfuscated
character_index = ((string_series != "\r") & (string_series != "\n"))
# Slice the string Series to get characters you want obfuscated
#    and obfuscate just those characters
obfuscated_series = myObfuscator.Obfuscate(string_series[character_index])
# You might need to modify either the above line or your obfuscator 
#     if the formats are incompatible
# Put the obfuscated characters back in the original Series at the
#    indices of the characters you obfuscated
string_series[character_index] = obfuscated_series
# Now put string_series in whatever format you want your output to be

This assumes by "preserve line breaks", you mean "keeps line breaks in the same place", not "obfuscate each line separately".

\$\endgroup\$
  • 1
    \$\begingroup\$ OP tagged C#, not Python. Would down vote, but I don't have the rep. \$\endgroup\$ – Simon Arsenault Oct 10 '17 at 19:35
  • 1
    \$\begingroup\$ @Simon Arsenault Whether a downvote is appropriate depends on whether the OP primarily wants code, or an approach. I presented an algorithm, explained what each step does, and presented Python code as an example. \$\endgroup\$ – Acccumulation Oct 10 '17 at 20:40
  • \$\begingroup\$ You can write it in a one liner: var obfuscatedText = new string(text.Select(i => new[] { '\n', '\r' }.Contains(i) ? i : obfuscater.Obfuscate(i.ToString())[0]).ToArray()). However, as you mentioned, the restriction is that each character will be obfuscated separately. \$\endgroup\$ – JanDotNet Oct 12 '17 at 12:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.