using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Windows.Forms; using System.Xml; using ChanSort.Api; namespace ChanSort.Loader.GlobalClone { class GcSerializer : SerializerBase { private readonly ChannelList atvChannels = new ChannelList(SignalSource.AnalogCT | SignalSource.TvAndRadio, "Analog"); private readonly ChannelList dtvTvChannels = new ChannelList(SignalSource.DvbCT | SignalSource.Tv, "DTV"); private readonly ChannelList dtvRadioChannels = new ChannelList(SignalSource.DvbCT | SignalSource.Radio, "Radio"); private readonly ChannelList satTvChannels = new ChannelList(SignalSource.DvbS | SignalSource.Tv, "Sat-TV"); private readonly ChannelList satRadioChannels = new ChannelList(SignalSource.DvbS | SignalSource.Radio, "Sat-Radio"); private XmlDocument doc; private readonly DvbStringDecoder dvbStringDecoder = new DvbStringDecoder(Encoding.Default); private string modelName; private readonly Dictionary satPositionByIndex = new Dictionary(); private bool usesBinaryDataInUtf8Envelope = false; #region ctor() public GcSerializer(string inputFile) : base(inputFile) { this.Features.ChannelNameEdit = ChannelNameEditMode.All; //this.Features.CanDeleteChannels = false; this.DataRoot.AddChannelList(this.atvChannels); this.DataRoot.AddChannelList(this.dtvTvChannels); this.DataRoot.AddChannelList(this.dtvRadioChannels); this.DataRoot.AddChannelList(this.satTvChannels); this.DataRoot.AddChannelList(this.satRadioChannels); } #endregion #region DisplayName public override string DisplayName => "LG GlobalClone loader"; #endregion #region Load() public override void Load() { bool fail = false; try { this.doc = new XmlDocument(); string textContent = File.ReadAllText(this.FileName, Encoding.UTF8); textContent = ReplaceInvalidXmlCharacters(textContent); var settings = new XmlReaderSettings { CheckCharacters = false }; using (var reader = XmlReader.Create(new StringReader(textContent), settings)) { doc.Load(reader); } } catch { fail = true; } var root = doc.FirstChild; if (root is XmlDeclaration) root = root.NextSibling; if (fail || root == null || root.LocalName != "TLLDATA") throw new FileLoadException("\"" + this.FileName + "\" is not a supported GlobalClone XML file"); foreach (XmlNode child in root.ChildNodes) { switch (child.LocalName) { case "ModelInfo": this.ReadModelInfo(child); break; case "SatelliteDB": this.ReadSatelliteDB(child); break; case "CHANNEL": this.ReadChannelLists(child); break; } } this.Features.ChannelNameEdit = usesBinaryDataInUtf8Envelope ? ChannelNameEditMode.Analog : ChannelNameEditMode.All; } #endregion #region ReadModelInfo() private void ReadModelInfo(XmlNode modelInfoNode) { // show warning about broken import function in early webOS firmware var regex = new System.Text.RegularExpressions.Regex(@"\d{2}([A-Z]{2})(\d{2})\d[0-9A-Z].*"); var series = ""; foreach (XmlNode child in modelInfoNode.ChildNodes) { switch (child.LocalName) { case "ModelName": this.modelName = child.InnerText; var match = regex.Match(this.modelName); if (match.Success) { series = match.Groups[1].Value; if ((series == "LB" || series == "UB") && StringComparer.InvariantCulture.Compare(match.Groups[2].Value, "60") >= 0) MessageBox.Show(Resources.GcSerializer_webOsFirmwareWarning, "LG GlobalClone", MessageBoxButtons.OK, MessageBoxIcon.Information); } break; } } // ask whether binary TLL file should be deleted var dir = Path.GetDirectoryName(this.FileName) ?? "."; var binTlls = Directory.GetFiles(dir, "xx" + series + "*.tll"); if (binTlls.Length > 0) { var txt = Resources.GcSerializer_ReadModelInfo_ModelWarning; if (MessageBox.Show(txt, "LG GlobalClone", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes) { foreach (var file in binTlls) File.Move(file, file + "_bak"); } } } #endregion #region ReadSatelliteDB() private void ReadSatelliteDB(XmlNode node) { foreach (XmlNode child in node.ChildNodes) { switch (child.LocalName) { case "SATDBInfo": this.ReadSatDbInfo(child); break; } } } private void ReadSatDbInfo(XmlNode node) { foreach (XmlNode child in node.ChildNodes) { switch (child.LocalName) { case "SatRecordInfo": int i = 0; foreach (XmlNode satNode in child.ChildNodes) this.ReadSatRecordInfo(i++, satNode); break; } } } private void ReadSatRecordInfo(int i, XmlNode satRecordInfoNode) { string orbitalPos = ""; foreach (XmlNode child in satRecordInfoNode.ChildNodes) { switch (child.LocalName) { case "Angle": orbitalPos += child.InnerText; break; case "AnglePrec": orbitalPos += "." + child.InnerText; break; case "DirEastWest": orbitalPos += child.InnerText == "0" ? "W" : "E"; break; } } this.satPositionByIndex[i] = orbitalPos; } #endregion #region ReadChannelLists() private void ReadChannelLists(XmlNode channelNode) { foreach (XmlNode chanListNode in channelNode.ChildNodes) { switch (chanListNode.LocalName) { case "ATV": this.ReadChannelList(chanListNode, true); break; case "DTV": this.ReadChannelList(chanListNode, false); break; case "DTVATV": // TODO: US DTV_ATSC files contain such lists break; } } } #endregion #region ReadChannelList() private void ReadChannelList(XmlNode node, bool analog) { int i = -1; foreach (XmlNode itemNode in node.ChildNodes) { if (itemNode.LocalName != "ITEM") continue; ++i; GcChannel ch = new GcChannel(analog ? SignalSource.AnalogCT | SignalSource.Tv : SignalSource.Digital, i, itemNode); this.ParseChannelInfoNodes(itemNode, ch); var list = this.DataRoot.GetChannelList(ch.SignalSource); this.DataRoot.AddChannel(list, ch); } } #endregion #region ParseChannelInfoNode() private void ParseChannelInfoNodes(XmlNode itemNode, ChannelInfo ch, bool onlyNames = false) { bool hasHexName = false; int mapType = 0; foreach (XmlNode info in itemNode.ChildNodes) { if (onlyNames && info.LocalName != "vchName" && info.LocalName != "hexVchName") continue; switch (info.LocalName) { // common to ATV and DTV case "prNum": ch.OldProgramNr = int.Parse(info.InnerText) & 0x3FFF; break; case "vchName": // In old file format versions, this field contains binary data stuffed into UTF8 envelopes. that data is correct // In newer file formats, this field contains plain text but fails to hold localized characters. The hexVchName field, if present, contains the correct data then. if (!hasHexName) ch.Name = ParseName(info.InnerText); break; case "sourceIndex": var source = int.Parse(info.InnerText); if (source == 2) ch.SignalSource |= SignalSource.Cable; else if (source == 7) ch.SignalSource |= SignalSource.Sat; else ch.SignalSource |= SignalSource.Antenna; break; case "mapType": mapType = int.Parse(info.InnerText); break; case "mapAttr": if (mapType == 1) ch.Favorites = (Favorites) int.Parse(info.InnerText); break; case "isBlocked": ch.Lock = int.Parse(info.InnerText) == 1; break; case "isSkipped": ch.Skip = int.Parse(info.InnerText) == 1; break; // ATV case "pllData": ch.FreqInMhz = (decimal) int.Parse(info.InnerText)/20; break; // DTV case "original_network_id": ch.OriginalNetworkId = int.Parse(info.InnerText); break; case "transport_id": ch.TransportStreamId = int.Parse(info.InnerText); break; case "service_id": ch.ServiceId = int.Parse(info.InnerText); break; case "serviceType": ch.ServiceType = int.Parse(info.InnerText); ch.SignalSource |= LookupData.Instance.IsRadioOrTv(ch.ServiceType); break; case "frequency": ch.FreqInMhz = int.Parse(info.InnerText); if ((ch.SignalSource & SignalSource.Sat) == 0) ch.FreqInMhz /= 1000; break; case "isInvisable": // that spelling error is part of the XML ch.Hidden = int.Parse(info.InnerText) == 1; break; case "isNumUnSel": // ? break; case "isDisabled": ch.IsDeleted = int.Parse(info.InnerText) != 0; break; case "usSatelliteHandle": int satIndex = int.Parse(info.InnerText); string satPos = this.satPositionByIndex.TryGet(satIndex); ch.SatPosition = satPos ?? satIndex.ToString(); // fallback to ensure unique UIDs ch.Satellite = satPos; break; // not present in all XML files. if present, the might be empty or corrupted case "hexVchName": var bytes = Tools.HexDecode(info.InnerText); string longName, shortName; dvbStringDecoder.GetChannelNames(bytes, 0, bytes.Length, out longName, out shortName); ch.Name = longName; ch.ShortName = shortName; hasHexName = true; break; } } } #endregion #region ParseName() private string ParseName(string input) { var bytes = Encoding.UTF8.GetBytes(input); if (bytes.Length == 0 || bytes[0] < 0xC0) return input; this.usesBinaryDataInUtf8Envelope = true; // older GlobalClone files look like as if the is Chinese, but it's a weired "binary inside UTF8 envelope" encoding: // A 3 byte UTF-8 envelope is used to encode 2 input bytes: 1110aaaa 10bbbbcc 10ccdddd represents the 16bit little endian integer aaaabbbbccccdddd, which represents bytes ccccdddd, aaaabbbb // If a remaining byte is >= 0x80, it is encoded in a 2 byte UTF-8 envelope: 110000aa 10aabbbb represents the byte aaaabbbb // If a remaining byte is < 0x80, it is encoded directly into a 1 byte UTF-8 char. (This can cause invalid XML files for values < 0x20.) using (MemoryStream ms = new MemoryStream(40)) { for (int i = 0, c = bytes.Length; i < c; i++) { int b0 = bytes[i + 0]; if (b0 >= 0xE0) // 3-byte UTF envelope for 2 input bytes { int b1 = bytes[i + 1]; int b2 = bytes[i + 2]; int ch1 = ((b1 & 0x03) << 6) | (b2 & 0x3F); int ch2 = ((b0 & 0x0F) << 4) | ((b1 & 0x3C) >> 2); ms.WriteByte((byte) ch1); ms.WriteByte((byte) ch2); i += 2; } else if (b0 >= 0xC0) // 2-byte UTF envelope for 1 input byte >= 0x80 { int b1 = bytes[i + 1]; int ch = ((b0 & 0x03) << 6) | (b1 & 0x3F); ms.WriteByte((byte)ch); i++; } else if (b0 < 0x80) // 1-byte UTF envelope for 1 input byte < 0x80 ms.WriteByte(bytes[i]); } string longName, shortName; this.dvbStringDecoder.GetChannelNames(ms.GetBuffer(), 0, (int)ms.Length, out longName, out shortName); return longName; } } #endregion #region Save() public override void Save(string tvOutputFile) { foreach (var list in this.DataRoot.ChannelLists) { foreach (var channel in list.Channels) { var ch = channel as GcChannel; if (ch == null) continue; // ignore proxy channels from reference lists var nameBytes = Encoding.UTF8.GetBytes(ch.Name); bool nameNeedsEncoding = nameBytes.Length != ch.Name.Length; string mapType = ""; foreach (XmlNode node in ch.XmlNode.ChildNodes) { switch (node.LocalName) { case "prNum": var nr = ch.NewProgramNr; if ((ch.SignalSource & SignalSource.Radio) != 0) nr |= 0x4000; node.InnerText = nr.ToString(); break; case "hexVchName": if (channel.IsNameModified) node.InnerText = (nameNeedsEncoding ? "15" : "") + Tools.HexEncode(nameBytes); // 0x15 = DVB encoding indicator for UTF-8 break; case "notConvertedLengthOfVchName": if (channel.IsNameModified) node.InnerText = ((nameNeedsEncoding ? 1 : 0) + ch.Name.Length).ToString(); break; case "vchName": if (channel.IsNameModified) node.InnerText = nameNeedsEncoding ? " " : ch.Name; if (node.InnerText == "") // XmlTextReader removed the required space from empty channel names node.InnerText = " "; break; case "isInvisable": node.InnerText = ch.Hidden ? "1" : "0"; break; case "isBlocked": node.InnerText = ch.Lock ? "1" : "0"; break; case "isSkipped": node.InnerText = ch.Skip ? "1" : "0"; break; case "isNumUnSel": // ? break; case "isDisabled": case "isDeleted": node.InnerText = ch.IsDeleted ? "1" : "0"; break; case "isUserSelCHNo": if (ch.NewProgramNr != ch.OldProgramNr) node.InnerText = "1"; break; case "mapType": mapType = node.InnerText; break; case "mapAttr": if (mapType == "1") node.InnerText = ((int) ch.Favorites).ToString(); break; } } } } // by default .NET reformats the whole XML. These settings produce the same format as the TV xml files use var settings = new XmlWriterSettings(); settings.Encoding = new UTF8Encoding(false); settings.Indent = true; settings.NewLineChars = "\r\n"; settings.NewLineHandling = NewLineHandling.Replace; settings.OmitXmlDeclaration = true; settings.IndentChars = ""; settings.CheckCharacters = false; using (StringWriter sw = new StringWriter()) using (XmlWriter xw = XmlWriter.Create(sw, settings)) { doc.Save(xw); xw.Flush(); string xml = RestoreInvalidXmlCharacters(sw.ToString()); xml = "\r\n\r\n" + xml; xml = xml.Replace("\r\n", "\r\n\r\n"); xml = xml.Replace("\r\n", "\r\n\r\n"); if (!xml.EndsWith("\r\n")) xml += "\r\n"; File.WriteAllText(tvOutputFile, xml, settings.Encoding); } } #endregion #region DefaultEncoding public override Encoding DefaultEncoding { get { return base.DefaultEncoding; } set { if (ReferenceEquals(value, this.DefaultEncoding)) return; base.DefaultEncoding = value; this.dvbStringDecoder.DefaultEncoding = value; this.ChangeEncoding(); } } #endregion #region ChangeEncoding() private void ChangeEncoding() { foreach (var list in this.DataRoot.ChannelLists) { foreach (var channel in list.Channels) { var gcChannel = channel as GcChannel; if (gcChannel != null) this.ParseChannelInfoNodes(gcChannel.XmlNode, channel, true); } } } #endregion #region ReplaceInvalidXmlCharacters() private string ReplaceInvalidXmlCharacters(string input) { StringBuilder output = new StringBuilder(); foreach (var c in input) { if (c >= ' ' || c == '\r' || c == '\n' || c == '\t') output.Append(c); else output.AppendFormat("&#x{0:d}{1:d};", c >> 4, c & 0x0F); } return output.ToString(); } #endregion #region RestoreInvalidXmlCharacters() private string RestoreInvalidXmlCharacters(string input) { StringBuilder output = new StringBuilder(); int prevIdx = 0; while(true) { int nextIdx = input.IndexOf("&#", prevIdx); if (nextIdx < 0) break; output.Append(input, prevIdx, nextIdx - prevIdx); int numBase = 10; char inChar; int outChar = 0; for (nextIdx += 2; (inChar=input[nextIdx]) != ';'; nextIdx++) { if (inChar == 'x' || inChar == 'X') numBase = 16; else outChar = outChar*numBase + HexNibble(inChar); } var binChar = (char)outChar; output.Append(binChar); prevIdx = nextIdx + 1; } output.Append(input, prevIdx, input.Length - prevIdx); return output.ToString(); } private int HexNibble(char hexDigit) { return hexDigit >= '0' && hexDigit <= '9' ? hexDigit - '0' : (Char.ToUpper(hexDigit) - 'A') + 10; } #endregion } }