commit 7048bdc21e3dfcd9728226b6fb4ac855798d50d9 Author: David Schultz Date: Thu Mar 23 19:43:29 2023 -0500 Initial commit diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..fa09e53 --- /dev/null +++ b/.classpath @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/.project b/.project new file mode 100644 index 0000000..d045561 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + irctokens + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..af7ce86 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,16 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..917a6d0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 David Schultz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..de17494 --- /dev/null +++ b/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + me.zpld + irctokens + 0.0.1-SNAPSHOT + irctokens + + + 17 + 17 + + \ No newline at end of file diff --git a/src/main/java/me/zpld/irctokens/Constants.java b/src/main/java/me/zpld/irctokens/Constants.java new file mode 100644 index 0000000..f15b8de --- /dev/null +++ b/src/main/java/me/zpld/irctokens/Constants.java @@ -0,0 +1,6 @@ +package me.zpld.irctokens; + +public class Constants { + public static final String[] TAG_UNESCAPED = {"\\", " ", ";", "\r", "\n"}; + public static final String[] TAG_ESCAPED = {"\\\\", "\\s", "\\:", "\\r", "\\n"}; +} diff --git a/src/main/java/me/zpld/irctokens/Formatting.java b/src/main/java/me/zpld/irctokens/Formatting.java new file mode 100644 index 0000000..3c69084 --- /dev/null +++ b/src/main/java/me/zpld/irctokens/Formatting.java @@ -0,0 +1,54 @@ +package me.zpld.irctokens; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class Formatting { + private static String escapeTag(String tag) { + for (int i = 0; i < Constants.TAG_UNESCAPED.length; i++) + tag = tag.replaceAll(Constants.TAG_UNESCAPED[i], Constants.TAG_ESCAPED[i]); + return tag; + } + + public static String format(HashMap tags, String source, String command, List params) throws Exception { + List outs = new ArrayList<>(); + if (tags != null && tags.size() > 0) { + List sTags = new ArrayList<>(); + + List tagKeys = Arrays.asList((String[])tags.keySet().toArray()); + Collections.sort(tagKeys); + for (String key : tagKeys) { + String value = tags.get(key); + if (value != null) + sTags.add(String.format("%s=%s", key, escapeTag(value))); + else + sTags.add(key); + } + outs.add("@" + String.join(";", sTags)); + } + + if (source != null) + outs.add(":"+source); + outs.add(command); + + if (params.size() > 0) { + String last = params.get(params.size()-1); + for (String param : params.subList(0, params.size()-1)) { + if (param.indexOf(" ") >= 0) + throw new Exception("Non-last params cannot have spaces"); + else if (param.startsWith(":")) + throw new Exception("Non-last params cannot start with colon"); + } + outs.addAll(params.subList(0, params.size()-1)); + if (last == null) + last = ":"; + else if (last.indexOf(" ") >= 0 || last.startsWith(":")) + last = ":" + last; + outs.add(last); + } + return String.join(" ", outs); + } +} diff --git a/src/main/java/me/zpld/irctokens/Line.java b/src/main/java/me/zpld/irctokens/Line.java new file mode 100644 index 0000000..b98dfa3 --- /dev/null +++ b/src/main/java/me/zpld/irctokens/Line.java @@ -0,0 +1,122 @@ +package me.zpld.irctokens; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +public class Line { + private HashMap tags; + private String source; + private String command; + private List params; + + public Line(HashMap tags, String source, String command, List params) { + this.tags = tags; + this.source = source; + this.command = command; + this.params = params; + } + + public Line(String command, String[] params) { + this(null, null, command, Arrays.asList(params)); + } + + @Override + public String toString() { + return String.format( + "Line(tags=%s, source=%s, command=%s, params=%s)", + tags, source, command, params + ); + } + + public HashMap getTags() { + return tags; + } + + public String getSource() { + return source; + } + + public String getCommand() { + return command; + } + + public List getParams() { + return params; + } + + public String getFormatted() { + try { + return Formatting.format(tags, source, command, params); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static String unescapeTag(String tag) { + String unescaped = ""; + char[] escaped = tag.toCharArray(); + int i = 0; + while (i < escaped.length) { + char current = escaped[i]; + if (current == '\\') { + if (i < escaped.length - 1) { + char next = escaped[i + 1]; + String duo = "" + current + next; + if (Arrays.asList(Constants.TAG_ESCAPED).contains(duo)) { + int index = Arrays.asList(Constants.TAG_ESCAPED).indexOf(duo); + unescaped += Constants.TAG_UNESCAPED[index]; + i += 2; + } else { + unescaped += next; + i += 2; + } + } else { + i++; + } + } else { + unescaped += current; + i++; + } + } + return unescaped; + } + + public static Line tokenise(String line) throws Exception { + HashMap tags = new HashMap<>(); + if (line.charAt(0) == '@') { + String[] tagParts = line.split(" ", 2); + String tags_s = tagParts[0]; + line = tagParts.length > 1 ? tagParts[1] : ""; + + for (String part : tags_s.substring(1).split(";")) { + String[] kv = part.split("=", 2); + String key = kv[0]; + String value = unescapeTag(kv.length > 1 ? kv[1] : ""); + tags.put(key, value); + } + } + + String[] lineParts = line.split(" :", 2); + line = lineParts[0]; + String trailing = lineParts.length > 1 ? lineParts[1] : ""; + List params = new ArrayList<>(Arrays.asList(line.split(" "))); + + String source = null; + if (params.size() > 0 && params.get(0).charAt(0) == ':') { + source = params.remove(0).substring(1); + } + + if (params.size() < 1) + throw new Exception("Cannot tokenise command-less line"); + String command = params.remove(0); + + if (trailing.length() > 0) + params.add(trailing); + + return new Line(tags, source, command, params); + } + +} diff --git a/src/main/java/me/zpld/irctokens/StatefulDecoder.java b/src/main/java/me/zpld/irctokens/StatefulDecoder.java new file mode 100644 index 0000000..ece0c8f --- /dev/null +++ b/src/main/java/me/zpld/irctokens/StatefulDecoder.java @@ -0,0 +1,68 @@ +package me.zpld.irctokens; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class StatefulDecoder { + private Charset encoding; + private Charset fallback; + private byte[] buffer; + + public StatefulDecoder(String encoding, String fallback) { + this.encoding = Charset.forName(encoding); + this.fallback = Charset.forName(fallback); + this.clear(); + } + + public StatefulDecoder() { + this("UTF-8", "ISO-8859-1"); + } + + public void clear() { + this.buffer = new byte[0]; + } + + public byte[] pending() { + return this.buffer; + } + + public List push(byte[] data) { + if (data == null || data.length == 0) { + return null; + } + + this.buffer = Arrays.copyOf(this.buffer, this.buffer.length + data.length); + System.arraycopy(data, 0, this.buffer, this.buffer.length - data.length, data.length); + List lines = new ArrayList<>(); + int start = 0; + int end = 0; + while (end < this.buffer.length) { + if (this.buffer[end] == '\n') { + byte[] lineBytes = Arrays.copyOfRange(this.buffer, start, end); + Line line = tokenise(lineBytes, this.encoding, this.fallback); + lines.add(line); + start = end + 1; + } + end++; + } + this.buffer = Arrays.copyOfRange(this.buffer, start, end); + return lines; + } + + private Line tokenise(byte[] lineBytes, Charset encoding, Charset fallback) { + String lineStr; + try { + lineStr = new String(lineBytes, encoding.name()); + } catch (Exception ex) { + lineStr = new String(lineBytes, fallback); + } + try { + return Line.tokenise(lineStr); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/me/zpld/irctokens/StatefulEncoder.java b/src/main/java/me/zpld/irctokens/StatefulEncoder.java new file mode 100644 index 0000000..5a5b80d --- /dev/null +++ b/src/main/java/me/zpld/irctokens/StatefulEncoder.java @@ -0,0 +1,69 @@ +package me.zpld.irctokens; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class StatefulEncoder { + private String encoding; + private byte[] buffer; + private List bufferedLines; + + public StatefulEncoder(String encoding) { + this.encoding = encoding; + clear(); + } + + public StatefulEncoder() { + this("UTF-8"); + } + + public void clear() { + buffer = new byte[0]; + bufferedLines = new ArrayList<>(); + } + + public byte[] pending() { + return buffer; + } + + public void push(Line line) { + String formattedLine = line.getFormatted() + "\r\n"; + byte[] encodedLine; + try { + encodedLine = formattedLine.getBytes(encoding); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return; + } + buffer = concat(buffer, encodedLine); + bufferedLines.add(line); + } + + public List pop(int byte_count) { + int sent = 0; + int index = 0; + while (sent < byte_count && index < buffer.length) { + if (buffer[index] == '\n') { + sent++; + } + index++; + } + byte[] sentBytes = new byte[index]; + System.arraycopy(buffer, 0, sentBytes, 0, index); + buffer = Arrays.copyOfRange(buffer, index, buffer.length); + List sentLines = new ArrayList<>(); + for (int i = 0; i < sent; i++) { + sentLines.add(bufferedLines.remove(0)); + } + return sentLines; + } + + private byte[] concat(byte[] a, byte[] b) { + byte[] result = new byte[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } +}