From 2c1062c085f11a3b947983854a0ee634d5e1e8ed Mon Sep 17 00:00:00 2001 From: shunf4 Date: Sat, 16 Nov 2019 01:14:19 +0800 Subject: [PATCH 1/6] fix:export index mismatch; correct subscription parser according to https://github.com/v2ray/v2ray-core/issues/1139 --- shadowray/__init__.py | 2 +- shadowray/subscribe/parser.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/shadowray/__init__.py b/shadowray/__init__.py index 37d349c..ebd6d46 100644 --- a/shadowray/__init__.py +++ b/shadowray/__init__.py @@ -249,7 +249,7 @@ def servers_export(index, path): j = parse_json_from_file(PROJECT_CONFIG_FILE) manager = Manager(server_file_name=j['servers_file'], binary=j['v2ray_binary']) - s = manager.get_server(index) + s = manager.get_server(index - 1) write_to_file(path, "w", json.dumps(s['config'])) diff --git a/shadowray/subscribe/parser.py b/shadowray/subscribe/parser.py index a8840cb..ed85a49 100644 --- a/shadowray/subscribe/parser.py +++ b/shadowray/subscribe/parser.py @@ -48,13 +48,18 @@ def get_url(self, url, **kwargs): port=int(t[1]['port'])) vmess_server.add_user(id=t[1]['id'], aid=int(t[1].get('aid', 0)), - security=t[1].get('type', 'auto'), - level=t[1].get('v', 0)) + security='auto', + level=int(t[1].get('level', 0))) vmess.add_server(vmess_server) outbound.set_settings(vmess) stream = Configuration.StreamSetting(type=Configuration.StreamSetting.STREAMSETTING, - network=t[1]['net']) + network=t[1]['net'], security=t[1].get('tls', 'auto')) + + stream.set_web_socket(Configuration.StreamSetting.WebSocket(t[1].get('path', '/'))) + masquerade_type = t[1].get('type', 'none') + stream.set_tcp(Configuration.StreamSetting.TCP(masquerade_type != 'none', masquerade_type)) + outbound.set_stream(stream) config.add_ontbound(outbound) From 7575d537b6b8a8ac6dab0edcfff4b999104eec97 Mon Sep 17 00:00:00 2001 From: shunf4 Date: Mon, 23 Dec 2019 21:27:48 +0800 Subject: [PATCH 2/6] Fix:grammar; Fix:import of bullet in setup.py --- .gitignore | 4 +++- shadowray/__init__.py | 5 ++++- shadowray/subscribe/parser.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 8ab9cec..4f52a9e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ v2ray venv dist -shadowray.egg* \ No newline at end of file +shadowray.egg* +/build/ +**/__pycache__/ diff --git a/shadowray/__init__.py b/shadowray/__init__.py index ebd6d46..ee67d1a 100644 --- a/shadowray/__init__.py +++ b/shadowray/__init__.py @@ -14,7 +14,10 @@ import platform import zipfile import os, stat, signal -from bullet import ScrollBar +try: + from bullet import ScrollBar +except ModuleNotFoundError: + pass def create_basic_config_file(): diff --git a/shadowray/subscribe/parser.py b/shadowray/subscribe/parser.py index ed85a49..9007d11 100644 --- a/shadowray/subscribe/parser.py +++ b/shadowray/subscribe/parser.py @@ -25,7 +25,7 @@ def get_url(self, url, **kwargs): text = text.split('\n') for t in text: - if len(t) is 0: + if len(t) == 0: continue t = t.split("://") From d4e1fb93019249ad43d44c5b0c6510b8110a6028 Mon Sep 17 00:00:00 2001 From: shunf4 Date: Sun, 5 Jan 2020 20:14:21 +0800 Subject: [PATCH 3/6] feat: supports ss link in sub link; allow rm sub; allow template --- shadowray/__init__.py | 63 +++++++++++++++- shadowray/config/v2ray.py | 1 + shadowray/config/version.py | 10 ++- shadowray/core/configuration.py | 15 ++-- shadowray/core/manager.py | 9 ++- shadowray/subscribe/parser.py | 129 +++++++++++++++++++++++++++----- 6 files changed, 195 insertions(+), 32 deletions(-) diff --git a/shadowray/__init__.py b/shadowray/__init__.py index 23c8771..760422c 100644 --- a/shadowray/__init__.py +++ b/shadowray/__init__.py @@ -29,6 +29,7 @@ def create_basic_config_file(): f.write('{ \ "v2ray_binary": null, \ "servers_file": null, \ + "template_file": null, \ "subscribe_file": null \ }') f.close() @@ -60,6 +61,10 @@ def have_config(): "Servers file not config.\nYou can config it by --config-servers path.Also, you can use --autoconfig to config it automatic.") return False + # if j['template_file'] is None: + # print( + # "Template file not config (this is optional).\nYou can config it by --config-template path.\nNote: This warning does not affect what you are doing next.") + return True @@ -73,6 +78,23 @@ def add_subscribe(args): manager.add_subscribe(v[0], v[1]) manager.save_subscribe() +def ls_subscribe(): + j = parse_json_from_file(PROJECT_CONFIG_FILE) + + manager = Manager(subscribe_file_name=j['subscribe_file']) + subscribes = manager.get_subscribe() + for k in subscribes.keys(): + sys.stdout.write(k + " " + subscribes[k] + "\n") + +def rm_subscribe(arg): + j = parse_json_from_file(PROJECT_CONFIG_FILE) + + manager = Manager(subscribe_file_name=j['subscribe_file']) + try: + subscribes = manager.rm_subscribe(arg) + return True + except KeyError as err: + sys.stderr.write("Error: subscription %s does not exist\n" % err.args[0]) def basic_config_v2ray(v2ray_binary=None): create_basic_config_file() @@ -116,6 +138,20 @@ def basic_config_servers(servers_file=None): write_to_file(PROJECT_CONFIG_FILE, 'w', json.dumps(j)) +def basic_config_template(template_file=None): + create_basic_config_file() + + f = open(PROJECT_CONFIG_FILE, 'r') + j = json.load(f) + f.close() + + if not template_file or template_file.strip() == "": + template_file = None + + j['template_file'] = template_file + + write_to_file(PROJECT_CONFIG_FILE, 'w', json.dumps(j)) + def download_latest_v2ray(): r = json.loads(requests.get(RELEASE_API).text) @@ -203,12 +239,12 @@ def auto_config(): if s: download_latest_v2ray() else: - print("Please setup by yourself.") + print("Please setup v2ray by yourself.") def update_subscribe(**kwargs): j = parse_json_from_file(PROJECT_CONFIG_FILE) - manager = Manager(server_file_name=j['servers_file'], subscribe_file_name=j['subscribe_file']) + manager = Manager(server_file_name=j['servers_file'], subscribe_file_name=j['subscribe_file'], template_file_name=j.get('template_file')) manager.update_subscribe(show_info=True, **kwargs) manager.save_servers() @@ -315,6 +351,16 @@ def main(): add_subscribe(op_value) break + if op_name in ("--subscribe-ls",): + if have_config() is True: + ls_subscribe() + break + + if op_name in ("--subscribe-rm",): + if have_config() is True: + rm_subscribe(op_value) + break + if op_name in ("--config-v2ray",): basic_config_v2ray(op_value) break @@ -327,16 +373,25 @@ def main(): basic_config_subscribe(op_value) break + if op_name in ("--config-template",): + basic_config_template(None if not op_value else op_value) + break + if op_name in ("--autoconfig",): auto_config() break if op_name in ("--subscribe-update",): port = 1082 + mux = 0 if "--port" in sys.argv: port = int(find_arg_in_opts(opts, "--port")) + if "--mux" in sys.argv: + mux = int(find_arg_in_opts(opts, "--mux")) + if mux < 0 or mux > 1024: + mux = 0 if have_config(): - update_subscribe(port=port) + update_subscribe(port=port, mux=mux) break if op_name in ("--list", "-l"): @@ -381,4 +436,6 @@ def main(): if have_config(): prepare_ping() +if __name__ == "__main__": + main() # TODO: configure single proxy by users diff --git a/shadowray/config/v2ray.py b/shadowray/config/v2ray.py index 633d919..5560866 100644 --- a/shadowray/config/v2ray.py +++ b/shadowray/config/v2ray.py @@ -5,6 +5,7 @@ USER_HOME = os.path.expanduser("~") SHADOWRAY_CONFIG_FOLDER = os.path.join(PROJECT_PATH, ".shadowray") +#SHADOWRAY_CONFIG_FOLDER = os.path.join(USER_HOME, ".shadowray") V2RAY_FOLDER = os.path.join(SHADOWRAY_CONFIG_FOLDER, "v2ray") V2RAY_BINARY = os.path.join(V2RAY_FOLDER, "v2ray") diff --git a/shadowray/config/version.py b/shadowray/config/version.py index e9e0dd2..5a88cd6 100644 --- a/shadowray/config/version.py +++ b/shadowray/config/version.py @@ -2,8 +2,8 @@ AUTHOR = "RMT" EMAIL = "d.rong@outlook.com" -COMMAND_LONG = ["version", "help", "subscribe-add=", "subscribe-update", "config-v2ray=", "config-subscribe=", - "config-servers=", "autoconfig", "subscribe-update", "list", "start=", "config-file=", "port=", +COMMAND_LONG = ["version", "help", "subscribe-add=", "subscribe-ls", "subscribe-rm=", "subscribe-update", "config-v2ray=", "config-subscribe=", + "config-servers=", "config-template=", "autoconfig", "subscribe-update", "list", "start=", "config-file=", "port=", "mux=", "servers-export=", "daemon", "stop", "v2ray-update", "ping"] COMMAND_SHORT = "vhs:lf:d" @@ -11,12 +11,16 @@ --help[-h] print help message --version[-v] show current version of shadowray --subscribe-add ',' add subscribe + --subscribe-ls list subscribes --subscribe-update update subscribe + --subscribe-rm delete a subscription (you will need a subscribe-update) --config-v2ray setup the path of v2ray binary --config-subscribe setup the path of subscribe file --config-servers setup the path of servers file + --config-template setup the path of template file (to cancel this setting, set path to "") --autoconfig setup basic setting automatically - --subscribe-update [--port ] update subscribe + --subscribe-update [--port ] [--mux <0/1-1024>] update subscribe (if template set, the configurations will be generated + according to the template, so the port is ignored) --list[-l] show all servers --start[-s] [-d|--daemon] start v2ray,the '-d or --daemon argument used to run v2ray as a daemon' --config-file[-f] run v2ray use the config file that provided by yourself diff --git a/shadowray/core/configuration.py b/shadowray/core/configuration.py index 5710509..0d5baa3 100644 --- a/shadowray/core/configuration.py +++ b/shadowray/core/configuration.py @@ -1,5 +1,5 @@ import json - +import copy class BaseConfig: def __init__(self, obj, **kwargs): @@ -21,11 +21,13 @@ def json_obj(self): class Configuration(BaseConfig): - def __init__(self): - self.__configuration = { + def __init__(self, default_conf=None): + if default_conf is None: + default_conf = { "inbounds": [], "outbounds": [] } + self.__configuration = copy.deepcopy(default_conf) super().__init__(self.__configuration) @@ -41,6 +43,9 @@ def add_inbound(self, inbound_obj): def add_ontbound(self, outbound_obj): self.__configuration['outbounds'].append(outbound_obj.json_obj) + def insert_outbound(self, i, outbound_obj): + self.__configuration['outbounds'].insert(i, outbound_obj.json_obj) + class Log(BaseConfig): DEBUG = "debug" INFO = "info" @@ -437,7 +442,7 @@ def add_client(self, id, level, aid, email): }) class Socks(BaseConfig): - def __init__(self, auth="noauth", udp=False, **kwargs): + def __init__(self, auth="noauth", udp=True, **kwargs): self.__socks = { "auth": auth, "udp": udp, @@ -507,7 +512,7 @@ def __init__(self, domain_strategy, redirect, user_level): super().__init__(self.__freedom) - class ShadowSocks(BaseConfig): + class Shadowsocks(BaseConfig): def __init__(self): self.__shadowsocks = { "servers": [] diff --git a/shadowray/core/manager.py b/shadowray/core/manager.py index 3405bdd..8ca521b 100644 --- a/shadowray/core/manager.py +++ b/shadowray/core/manager.py @@ -6,9 +6,9 @@ class Manager: - def __init__(self, subscribe_file_name=None, server_file_name=None, binary=None): + def __init__(self, subscribe_file_name=None, server_file_name=None, binary=None, template_file_name=None): if subscribe_file_name is not None: - self.__subscribe = Parser(filename=subscribe_file_name) + self.__subscribe = Parser(filename=subscribe_file_name, template=template_file_name) if server_file_name is not None: self.__server = Server(filename=server_file_name) @@ -19,6 +19,9 @@ def __init__(self, subscribe_file_name=None, server_file_name=None, binary=None) def add_subscribe(self, name, url): self.__subscribe.add(name, url) + def get_subscribe(self): + return self.__subscribe.subscribes + def update_subscribe(self, show_info=False, **kwargs): self.__subscribe.update(show_info=show_info, **kwargs) @@ -30,7 +33,7 @@ def update_subscribe(self, show_info=False, **kwargs): self.__server.add(protocol=i['protocol'], config=i['config'], ps=i['ps'], key=SERVER_KEY_FROM_SUBSCRIBE, host=i['host']) - def delete_subscribe(self, name): + def rm_subscribe(self, name): self.__subscribe.delete(name) def show_servers(self): diff --git a/shadowray/subscribe/parser.py b/shadowray/subscribe/parser.py index 9007d11..f1858d3 100644 --- a/shadowray/subscribe/parser.py +++ b/shadowray/subscribe/parser.py @@ -3,10 +3,34 @@ from shadowray.common.B64 import decode from shadowray.config.v2ray import SUBSCRIBE_FILE from shadowray.core.configuration import Configuration - - +import urllib.parse +import itertools +import base64 + +def base64_decode(x): + #if debug: eprint(x) + return base64.urlsafe_b64decode(x + '=' * (-len(x) % 4)).decode("utf-8") + +def urlsafe_base64_decode(x): + #if debug: eprint(x) + return base64.urlsafe_b64decode(x + '=' * (-len(x) % 4)).decode("utf-8") + +def compat_base64_decode(x): + try: + return base64_decode(x) + except Exception: + return urlsafe_base64_decode(x) + +def urlsafe_base64_encode(x): + if debug: eprint(x) + r = base64.urlsafe_b64encode(x.encode("utf-8")).decode("ascii") + if debug: eprint(r) + while r and r[-1] == '=': + r = r[:-1] + if debug: eprint(r) + return r class Parser: - def __init__(self, filename=None): + def __init__(self, filename=None, template=None): self.servers = [] self.filename = None @@ -18,31 +42,45 @@ def __init__(self, filename=None): self.filename = filename + if template is not None: + f = open(template, "r") + self.template = json.load(f) + f.close() + else: + self.template = None + def get_url(self, url, **kwargs): r = requests.get(url).text text = decode(r) text = text.split('\n') + mux = 0 + if kwargs.get("mux") is not None: + mux = kwargs.get("mux") + for t in text: if len(t) == 0: continue + original_t = t t = t.split("://") - t[1] = json.loads(decode(t[1])) + if self.template: + config = Configuration(self.template) + else: + config = Configuration() - config = Configuration() - - port = 1082 - if kwargs.get("port") is not None: - port = kwargs.get("port") - inbound = Configuration.Inbound(port, "127.0.0.1", "socks") - socks = Configuration.ProtocolSetting.Inbound.Socks() - inbound.set_settings(socks) - config.add_inbound(inbound) + port = 1082 + if kwargs.get("port") is not None: + port = kwargs.get("port") + inbound = Configuration.Inbound(port, "127.0.0.1", "socks") + socks = Configuration.ProtocolSetting.Inbound.Socks() + inbound.set_settings(socks) + config.add_inbound(inbound) if t[0] == "vmess": - outbound = Configuration.Outbound("vmess") + t[1] = json.loads(decode(t[1])) + outbound = Configuration.Outbound("vmess", "proxy") vmess = Configuration.ProtocolSetting.Outbound.VMess() vmess_server = Configuration.ProtocolSetting.Outbound.VMess.Server(addr=t[1]['add'], port=int(t[1]['port'])) @@ -61,14 +99,69 @@ def get_url(self, url, **kwargs): stream.set_tcp(Configuration.StreamSetting.TCP(masquerade_type != 'none', masquerade_type)) outbound.set_stream(stream) - config.add_ontbound(outbound) + outbound.set_mux(Configuration.Outbound.Mux(mux != 0, mux)) - self.servers.append({ + server_obj = { "protocol": t[0], - "config": config.json_obj, + "config": None, "ps": t[1]['ps'], "host": t[1]['add'] - }) + } + + if t[0] == "ss": + outbound = Configuration.Outbound("shadowsocks", "proxy") + ss = Configuration.ProtocolSetting.Outbound.Shadowsocks() + + tmp1 = original_t[len("ss://"):].split('#') + if len(tmp1) < 2: + tmp1.append("") + tmp_ps = urllib.parse.unquote(tmp1[-1]) + + ss_body = '#'.join(tmp1[:-1]) + if not ('@' in ss_body): + ss_body = compat_base64_decode(ss_body) + + tmp2 = ss_body.split("@") + if ':' in tmp2[0]: + ss_user = tmp2[0] + else: + ss_user = compat_base64_decode(tmp2[0]) + + ss_enc, ss_password = ss_user.split(":") + ss_add, ss_port = tmp2[1].split(":") + + ss_port = ''.join(itertools.takewhile(str.isdigit, ss_port)) + ss_ps = ("%s:%s" % (ss_add, ss_port)) if not tmp_ps else tmp_ps.strip() + + ss_obj = { + "is_ss": True, + "add": ss_add, + "port": ss_port, + "enc": ss_enc, + "password": ss_password, + "ps": ss_ps + } + + ss.add_server(str(ss_obj["add"]), int(ss_obj["port"]), str(ss_obj["enc"]), str(ss_obj["password"]), 0) + + outbound.set_settings(ss) + + stream = Configuration.StreamSetting(type=Configuration.StreamSetting.STREAMSETTING, + network="tcp", security="none") + + outbound.set_stream(stream) + + server_obj = { + "protocol": t[0], + "config": None, + "ps": ss_ps, + "host": ss_add + } + + + config.insert_outbound(0, outbound) + server_obj["config"] = config.json_obj + self.servers.append(server_obj) def update(self, name=None, show_info=False, **kwargs): self.servers.clear() From 22dfbbf43c090830e671334e2b67fcc182d179c7 Mon Sep 17 00:00:00 2001 From: shunf4 Date: Sat, 28 Mar 2020 16:42:50 +0800 Subject: [PATCH 4/6] update --- shadowray/__init__.py | 16 +++++++++++++--- shadowray/subscribe/parser.py | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/shadowray/__init__.py b/shadowray/__init__.py index 23c8771..bc05123 100644 --- a/shadowray/__init__.py +++ b/shadowray/__init__.py @@ -332,11 +332,21 @@ def main(): break if op_name in ("--subscribe-update",): - port = 1082 + socks_port = 1082 + http_port = 8118 + listen_addr = "127.0.0.1" + if "--port" in sys.argv: - port = int(find_arg_in_opts(opts, "--port")) + socks_port = int(find_arg_in_opts(opts, "--port")) + if "--socks-port" in sys.argv: + socks_port = int(find_arg_in_opts(opts, "--socks-port")) + if "--http-port" in sys.argv: + http_port = int(find_arg_in_opts(opts, "--http-port")) + if "--listen-port" in sys.argv: + listen_addr = find_arg_in_opts(opts, "--listen-port") + if have_config(): - update_subscribe(port=port) + update_subscribe(socks_port=socks_port, http_port=http_port, listen_addr=listen_addr) break if op_name in ("--list", "-l"): diff --git a/shadowray/subscribe/parser.py b/shadowray/subscribe/parser.py index 9007d11..7b87952 100644 --- a/shadowray/subscribe/parser.py +++ b/shadowray/subscribe/parser.py @@ -33,14 +33,26 @@ def get_url(self, url, **kwargs): config = Configuration() - port = 1082 - if kwargs.get("port") is not None: - port = kwargs.get("port") - inbound = Configuration.Inbound(port, "127.0.0.1", "socks") + socks_port = 1082 + http_port = 8118 + listen_addr = "127.0.0.1" + if kwargs.get("socks_port") is not None: + socks_port = kwargs.get("socks_port") + if kwargs.get("http_port") is not None: + http_port = kwargs.get("http_port") + if kwargs.get("listen_addr") is not None: + listen_addr = kwargs.get("listen_addr") + + inbound = Configuration.Inbound(socks_port, listen_addr, "socks") socks = Configuration.ProtocolSetting.Inbound.Socks() inbound.set_settings(socks) config.add_inbound(inbound) + inbound = Configuration.Inbound(http_port, listen_addr, "http") + http = Configuration.ProtocolSetting.Inbound.Http(0) + inbound.set_settings(http) + config.add_inbound(inbound) + if t[0] == "vmess": outbound = Configuration.Outbound("vmess") vmess = Configuration.ProtocolSetting.Outbound.VMess() From e2caf387638450c0dd162b541bddf40e80047b1d Mon Sep 17 00:00:00 2001 From: shunf4 Date: Sat, 28 Mar 2020 16:51:21 +0800 Subject: [PATCH 5/6] update --- shadowray/__init__.py | 4 ++-- shadowray/config/version.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/shadowray/__init__.py b/shadowray/__init__.py index 8dd70c0..2f234b1 100644 --- a/shadowray/__init__.py +++ b/shadowray/__init__.py @@ -393,8 +393,8 @@ def main(): socks_port = int(find_arg_in_opts(opts, "--socks-port")) if "--http-port" in sys.argv: http_port = int(find_arg_in_opts(opts, "--http-port")) - if "--listen-port" in sys.argv: - listen_addr = find_arg_in_opts(opts, "--listen-port") + if "--listen-addr" in sys.argv: + listen_addr = find_arg_in_opts(opts, "--listen-addr") if "--mux" in sys.argv: mux = int(find_arg_in_opts(opts, "--mux")) if mux < 0 or mux > 1024: diff --git a/shadowray/config/version.py b/shadowray/config/version.py index 5a88cd6..3c83d67 100644 --- a/shadowray/config/version.py +++ b/shadowray/config/version.py @@ -1,4 +1,4 @@ -VERSION_ID = "0.1.6" +VERSION_ID = "0.1.7" AUTHOR = "RMT" EMAIL = "d.rong@outlook.com" @@ -19,8 +19,10 @@ --config-servers setup the path of servers file --config-template setup the path of template file (to cancel this setting, set path to "") --autoconfig setup basic setting automatically - --subscribe-update [--port ] [--mux <0/1-1024>] update subscribe (if template set, the configurations will be generated - according to the template, so the port is ignored) + --subscribe-update [--socks-port ] update subscribe (if template set, the configurations will be generated + [--http-port ] according to the template, so the port is ignored) + [--mux <0/1-1024>] + [--listen-addr <127.0.0.1>] --list[-l] show all servers --start[-s] [-d|--daemon] start v2ray,the '-d or --daemon argument used to run v2ray as a daemon' --config-file[-f] run v2ray use the config file that provided by yourself From 3ec2e69a9b079e051983f7d84252ba787ce933a2 Mon Sep 17 00:00:00 2001 From: shunf4 Date: Sat, 28 Mar 2020 16:58:00 +0800 Subject: [PATCH 6/6] update --- shadowray/config/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shadowray/config/version.py b/shadowray/config/version.py index 3c83d67..cdf71b2 100644 --- a/shadowray/config/version.py +++ b/shadowray/config/version.py @@ -3,7 +3,7 @@ EMAIL = "d.rong@outlook.com" COMMAND_LONG = ["version", "help", "subscribe-add=", "subscribe-ls", "subscribe-rm=", "subscribe-update", "config-v2ray=", "config-subscribe=", - "config-servers=", "config-template=", "autoconfig", "subscribe-update", "list", "start=", "config-file=", "port=", "mux=", + "config-servers=", "config-template=", "autoconfig", "subscribe-update", "list", "start=", "config-file=", "port=", "socks-port=", "http-port=", "listen-addr=", "mux=", "servers-export=", "daemon", "stop", "v2ray-update", "ping"] COMMAND_SHORT = "vhs:lf:d"