/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.RegularExpressions; using System.Xml; //using log4net; namespace OpenSim.Region.Framework.Scenes { public class LinksetData { public int m_MemoryLimit; public int m_MemoryUsed; private readonly object linksetDataLock = new(); public Dictionary Data; public LinksetData(int limit) { Data = new Dictionary(); m_MemoryLimit = limit; m_MemoryUsed = 0; } public LinksetData(int limit, int used) { Data = new Dictionary(); m_MemoryLimit = limit; m_MemoryUsed = used; } public LinksetData Copy() { lock (linksetDataLock) { var copy = new LinksetData(m_MemoryLimit, m_MemoryUsed); foreach (var entry in Data) { LinksetDataEntry val = entry.Value.Copy(); copy.Data[entry.Key] = val; } return copy; } } /// /// Adds or updates a entry to linkset data with optional password /// /// /// return values must match values expected by LSL /// public int AddOrUpdate(string key, string value, string pass) { int deltaMem; lock (linksetDataLock) { if (Data.TryGetValue(key, out LinksetDataEntry entry)) { if (!entry.CheckPassword(pass)) return 3; if (entry.Value == value) return 5; deltaMem = value.Length - entry.Value.Length; if ((m_MemoryUsed + deltaMem) > m_MemoryLimit) return 1; m_MemoryUsed += deltaMem; if(m_MemoryUsed < 0) m_MemoryUsed = 0; entry.Value = value; return 0; } deltaMem = value.Length + key.Length; if (!string.IsNullOrEmpty(pass)) deltaMem += pass.Length; if ((m_MemoryUsed + deltaMem) > m_MemoryLimit) return 1; m_MemoryUsed += deltaMem; Data[key] = new LinksetDataEntry() { Value = value, Password = pass }; return 0; } } public int AddOrUpdate(string key, string value) { int deltaMem; lock (linksetDataLock) { if (Data.TryGetValue(key, out LinksetDataEntry entry)) { if (entry.IsProtected) return 3; if (entry.Value == value) return 5; deltaMem = value.Length - entry.Value.Length; if ((m_MemoryUsed + deltaMem) > m_MemoryLimit) return 1; entry.Value = value; m_MemoryUsed += deltaMem; return 0; } deltaMem = value.Length + key.Length; if ((m_MemoryUsed + deltaMem) > m_MemoryLimit) return 1; m_MemoryUsed += deltaMem; Data[key] = new LinksetDataEntry() { Value = value, Password = null }; return 0; } } /// /// Deletes a named key from the key value store /// /// The key value we're removing /// The password for a protected field (or string.Empty if not protected) /// /// return values must match values expected by LSL /// public int Remove(string key, string pass) { lock (linksetDataLock) { if (Data.Count <= 0) return 4; if (!Data.TryGetValue(key, out LinksetDataEntry entry)) return 4; if (!entry.CheckPassword(pass)) return 3; Data.Remove(key); m_MemoryUsed -= key.Length + entry.Value.Length; if (!string.IsNullOrEmpty(entry.Password)) m_MemoryUsed -= entry.Password.Length; if (m_MemoryUsed < 0) m_MemoryUsed = 0; return 0; } } public int Remove(string key) { lock (linksetDataLock) { if (Data.Count <= 0) return 4; if(string.IsNullOrEmpty(key)) return 4; if (!Data.TryGetValue(key, out LinksetDataEntry entry)) return 4; if (entry.IsProtected) return 3; Data.Remove(key); m_MemoryUsed -= key.Length + entry.Value.Length; if (m_MemoryUsed < 0) m_MemoryUsed = 0; return 0; } } public string Get(string key, string pass) { lock (linksetDataLock) { return (Data.TryGetValue(key, out LinksetDataEntry entry) && entry.CheckPassword(pass)) ? entry.Value : string.Empty; } } public string Get(string key) { lock (linksetDataLock) { return (Data.TryGetValue(key, out LinksetDataEntry entry) && entry.IsNotProtected) ? entry.Value : string.Empty; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasData() { return Data.Count > 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Count() { return Data.Count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Free() { int free = m_MemoryLimit - m_MemoryUsed; return free > 0 ? free : 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Used() { return m_MemoryUsed; } public string[] RemoveByPattern(string pattern, string pass, out int notDeleted) { notDeleted = 0; List ret; lock (linksetDataLock) { if (Data.Count <= 0) return Array.Empty(); try { ret = new List(); Regex reg = new(pattern, RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(1)); foreach (var kvp in Data) { if (reg.IsMatch(kvp.Key)) { if (kvp.Value.CheckPassword(pass)) { int mem = kvp.Value.Value.Length + kvp.Key.Length; if(kvp.Value.IsProtected) mem += kvp.Value.Password.Length; m_MemoryUsed -= mem; ret.Add(kvp.Key); } else notDeleted++; } } } catch { notDeleted = 0; return Array.Empty(); } foreach (string k in ret) Data.Remove(k); if (m_MemoryUsed < 0) m_MemoryUsed = 0; return ret.ToArray(); } } public int CountByPattern(string pattern) { lock (linksetDataLock) { if (Data.Count <= 0) return 0; try { Regex reg = new(pattern, RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(1)); int ret = 0; foreach (string k in Data.Keys) { if (reg.IsMatch(k)) ret++; } return ret; } catch { return 0; } } } public string[] ListKeysByPatttern(string pattern, int start, int count) { List lkeys; lock (linksetDataLock) { if (Data.Count <= 0 || start >= Data.Count) return Array.Empty(); try { Regex reg = new(pattern, RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(1)); lkeys = new(Data.Count); foreach (string k in Data.Keys) { if (reg.IsMatch(k)) lkeys.Add(k); } } catch { return Array.Empty(); } } if (lkeys.Count == 0) return Array.Empty(); lkeys.Sort(); if (start < 0) start = 0; if (count < 1 || start + count > lkeys.Count) count = lkeys.Count - start; List result = lkeys.GetRange(start, count); return result.ToArray(); } public string[] ListKeys(int start, int count) { string[] keys; lock (linksetDataLock) { if (Data.Count <= 0 || start >= Data.Count) return Array.Empty(); keys = new string[Data.Count]; Data.Keys.CopyTo(keys, 0); } Array.Sort(keys); if (start < 0) start = 0; if (count < 1) return keys[start..]; int end = start + count; if (end >= keys.Length) return keys[start..]; return keys[start..end]; } /// /// Merge the linksetData present in another Linkset into this one. /// If a key is present in our linksetData it wins, dont overide it. /// /// public void MergeOther(LinksetData otherLinksetData) { if (otherLinksetData is null || otherLinksetData.Data is null || otherLinksetData.Count() == 0) return; lock (linksetDataLock) { if(m_MemoryUsed + otherLinksetData.Used() < m_MemoryLimit) { foreach (var kvp in otherLinksetData.Data) { if (Data.TryAdd(kvp.Key, kvp.Value)) { m_MemoryUsed += kvp.Key.Length + kvp.Value.Value.Length; if (!string.IsNullOrEmpty(kvp.Value.Password)) m_MemoryUsed += kvp.Value.Password.Length; } } return; } SortedList otherOrdered = new(otherLinksetData.Data); foreach (var kvp in otherOrdered) { int mem = kvp.Key.Length + kvp.Value.Value.Length; if (!string.IsNullOrEmpty(kvp.Value.Password)) mem += kvp.Value.Password.Length; if (m_MemoryUsed + mem >= m_MemoryLimit) return; if (Data.TryAdd(kvp.Key, kvp.Value)) { m_MemoryUsed += mem; if (!string.IsNullOrEmpty(kvp.Value.Password)) m_MemoryUsed += kvp.Value.Password.Length; } } otherLinksetData.Data = null; otherLinksetData.m_MemoryUsed = 0; } } /// /// ResetLinksetData - clear the list and update the accounting. /// public void ResetLinksetData() { lock (linksetDataLock) { if (Data.Count <= 0) return; Data.Clear(); m_MemoryUsed = 0; } } public string ToJson() { lock (linksetDataLock) { return JsonSerializer.Serialize>(Data); } } public void ToXML(XmlTextWriter writer) { if (Data.Count < 1) return; using MemoryStream ms = new(m_MemoryUsed); ToBin(ms); if (ms.Length < 1) return; writer.WriteStartElement("lnkstdt"); writer.WriteBase64(ms.GetBuffer(), 0, (int)ms.Length); writer.WriteEndElement(); } public static LinksetData FromXML(ReadOnlySpan data) { if (data.Length < 8) return null; int minLength = ((data.Length * 3) + 3) / 4; byte[] bindata = ArrayPool.Shared.Rent(minLength); try { if (Convert.TryFromBase64Chars(data, bindata, out int bytesWritten)) return FromBin(bindata); } catch { } finally { ArrayPool.Shared.Return(bindata); } return null; } public byte[] ToBin() { if(Data.Count < 1) return null; using MemoryStream ms = new(m_MemoryUsed); ToBin(ms); return ms.Length > 0 ? ms.ToArray() : null; } public void ToBin(MemoryStream ms) { try { using BinaryWriter bw = new BinaryWriter(ms, System.Text.Encoding.UTF8, true); bw.Write((byte)1); // storage version bw.Write7BitEncodedInt(m_MemoryLimit); lock (linksetDataLock) { bw.Write7BitEncodedInt(Data.Count); foreach (var kvp in Data) { bw.Write(kvp.Key); bw.Write(kvp.Value.Value); if(kvp.Value.IsProtected) bw.Write(kvp.Value.Password); else bw.Write((byte)0); } } return; } catch { } ms.SetLength(0); } public static LinksetData FromBin(byte[] data) { if (data.Length < 8) return null; try { using BinaryReader br = new BinaryReader(new MemoryStream(data)); int version = br.Read7BitEncodedInt(); int memoryLimit = br.Read7BitEncodedInt(); if(memoryLimit < 0 || memoryLimit > 256 * 1024) memoryLimit = 256 * 1024; int count = br.Read7BitEncodedInt(); if(count == 0) return null; LinksetData ld = new LinksetData(memoryLimit); for(int i = 0; i < count; i++) { string key = br.ReadString(); if (key.Length == 0) continue; ld.m_MemoryUsed += key.Length; string value = br.ReadString(); if(value.Length == 0) continue; ld.m_MemoryUsed += value.Length; string pass = br.ReadString(); ld.m_MemoryUsed += pass.Length; if(ld.m_MemoryUsed > memoryLimit) break; if(pass.Length > 0) { ld.Data[key] = new LinksetDataEntry() { Value = value, Password = pass }; } else { ld.Data[key] = new LinksetDataEntry() { Value = value, Password = null }; } } return ld; } catch { } return null; } } public class LinksetDataEntry { public string Password; public string Value; public LinksetDataEntry() { } public LinksetDataEntry(string value, string password) { Value = value; Password = password; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CheckPassword(string pass) { // A undocumented caveat for LinksetData appears to be that even for unprotected values, // if a pass is provided, it is still treated as protected return string.IsNullOrEmpty(Password) || (Password == pass); } public LinksetDataEntry Copy() { return new LinksetDataEntry { Password = Password, Value = Value }; } public bool IsProtected { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return !string.IsNullOrEmpty(Password); } } public bool IsNotProtected { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return string.IsNullOrEmpty(Password); } } } }