SMTPSyntaxImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Hal Murray July 2, 1985 2:31:00 am PDT
Last Edited by: HGM, May 8, 1985 0:30:18 am PDT
Last Edited by: DCraft, November 22, 1983 1:59 pm
Last Edited by: Taft, February 3, 1984 1:17:33 pm PST
John Larson, July 12, 1987 2:35:52 am PDT
DIRECTORY
Basics USING [bytesPerWord],
GVBasics USING [ItemHeader, RopeFromTimestamp, Timestamp],
GVProtocol USING [Failed, ReceiveCount, ReceiveItemHeader, ReceiveRName, ReceiveTimestamp],
IO USING [EndOfStream, GetBlock, GetIndex, PutBlock, PutChar, PutF, PutRope, SetIndex, STREAM],
IPConfig USING [bitnetGateway, uucpGateway, csnetGateway, mailnetGateway, validDomains],
RefText USING [AppendChar, ObtainScratch, ReleaseScratch],
Rope USING [Cat, Concat, Equal, Fetch, Find, FromChar, FromRefText, IsEmpty, Length, ROPE, Substr, Translate],
IPName USING [LoadCacheFromName, NormalizeName],
SMTPControl USING [defaultRegistry, xeroxDomain],
SMTPSupport USING [CreateSubrangeStream, Log],
SMTPSyntax USING [GVItemProc];
SMTPSyntaxImpl:
CEDAR PROGRAM
IMPORTS GVBasics, GVProtocol, IO, RefText, Rope, IPConfig, IPName, SMTPControl, SMTPSupport
EXPORTS SMTPSyntax =
BEGIN
STREAM: TYPE = IO.STREAM;
ROPE: TYPE = Rope.ROPE;
bitnetGateway: Rope.ROPE ← IPConfig.bitnetGateway;
uucpGateway: Rope.ROPE ← IPConfig.uucpGateway;
csnetGateway: Rope.ROPE ← IPConfig.csnetGateway;
mailnetGateway: Rope.ROPE ← IPConfig.mailnetGateway;
EnumerateGVItems:
PUBLIC
PROC [GVStream:
STREAM, proc: SMTPSyntax.GVItemProc,
procData:
REF
ANY ←
NIL] = {
nextItemIndex: INT;
continue: BOOL;
itemHeader: GVBasics.ItemHeader;
DO
itemHeader ← GVProtocol.ReceiveItemHeader[GVStream];
nextItemIndex ← GVStream.GetIndex[] +
(itemHeader.length+ bpw-1)/bpw*bpw --word boundary--;
continue ← proc[itemHeader, GVStream, procData];
IF (itemHeader.type = LastItem) OR (NOT continue) THEN EXIT;
GVStream.SetIndex[nextItemIndex];
ENDLOOP;
};
bpw: INT = Basics.bytesPerWord;
ReceiveRName:
PUBLIC
PROC[GVStream:
STREAM]
RETURNS [
ROPE] = {
ENABLE GVProtocol.Failed =>
IF why = protocolError
THEN
ERROR SyntaxError[Rope.Concat["failed to read RName: ", text]];
RETURN[GVProtocol.ReceiveRName[GVStream]]; };
ReceiveCount:
PUBLIC
PROC[GVStream:
STREAM]
RETURNS [
CARDINAL] = {
ENABLE GVProtocol.Failed =>
IF why = protocolError
THEN
ERROR SyntaxError[Rope.Concat["failed to read count: ", text]];
RETURN[LOOPHOLE[GVProtocol.ReceiveCount[GVStream]]]; };
SyntaxError: PUBLIC ERROR [reason: ROPE] ~ CODE;
PrintGVItem:
PUBLIC SMTPSyntax.GVItemProc = {
out: STREAM ~ NARROW[procData];
BEGIN
ENABLE {
IO.EndOfStream => {out.PutRope["<<<unexpected EOS>>>\n"]; GOTO Return};
SyntaxError => {
out.PutRope["<<<syntax error: "]; out.PutRope[reason]; out.PutRope[">>>\n"];
GOTO Return; }; };
PutHeader:
PROC [type:
ROPE, raw:
BOOL] =
TRUSTED {
out.PutF["----- %g (%bB), %g bytes", [rope[type]], [integer[LOOPHOLE[itemHeader.type, CARDINAL]]], [integer[itemHeader.length]] ];
out.PutRope[IF raw THEN " (raw format) -----\n" ELSE " -----\n"];
IF itemHeader.length <= 0 THEN ERROR SyntaxError["length <= 0"]; };
PutRaw:
PROC [] = {
-- somewhat inefficient, but infrequently used
currentIndex: INT = itemStream.GetIndex[];
nBytesLeft: INT ← itemHeader.length;
itemRestrictedStream: STREAM = SMTPSupport.CreateSubrangeStream[itemStream, currentIndex, currentIndex + nBytesLeft];
buffer: REF TEXT = RefText.ObtainScratch[100];
WHILE nBytesLeft > 0
DO
Beware of bounds fault - count is a NAT
nBytesRead:
INT ← itemRestrictedStream.GetBlock[
buffer, 0, MIN[nBytesLeft, buffer.maxLength]];
IF nBytesRead = 0 THEN ERROR IO.EndOfStream[itemRestrictedStream];
out.PutBlock[buffer, 0, nBytesRead];
nBytesLeft ← nBytesLeft - nBytesRead;
ENDLOOP;
out.PutChar['\n];
RefText.ReleaseScratch[buffer]; };
SELECT itemHeader.type
FROM
PostMark => {
PutHeader["PostMark", FALSE];
out.PutRope[GVBasics.RopeFromTimestamp[GVProtocol.ReceiveTimestamp[itemStream]]];
out.PutChar['\n];};
Sender => {
PutHeader["Sender", FALSE];
out.PutRope[ReceiveRName[itemStream]];
out.PutChar['\n];};
ReturnTo => {
PutHeader["ReturnTo", FALSE];
out.PutRope[ReceiveRName[itemStream]];
out.PutChar['\n];};
Recipients => {
numRecips: INT = ReceiveCount[itemStream];
PutHeader["Recipients", FALSE];
THROUGH [1..numRecips]
DO
out.PutRope[ReceiveRName[itemStream]]; out.PutChar['\n] ENDLOOP;};
Text => {PutHeader["Text", FALSE]; PutRaw[]};
Capability => {PutHeader["Capability", TRUE]; PutRaw[]};
Audio => {PutHeader["Audio", TRUE]; out.PutRope["<probably not worth printing>\n"]};
LastItem => {PutHeader["LastItem", FALSE]; out.PutChar['\n]};
ENDCASE => {PutHeader["", TRUE]; PutRaw[]};
EXITS Return => NULL;
END; };
BlessReturnPath:
PUBLIC
PROC [raw:
ROPE]
RETURNS [arpa:
ROPE] =
BEGIN
1) Bitch if name of first host on return path isn't recognized by name servers
2) Make sure it ends in .ARPA (or such) so GV will send rejections back via us
length: INT = Rope.Length[raw];
host: ROPE;
IF Rope.Fetch[raw, 0] = '@
THEN {
-- @Foo:X@Y case
FOR i:
INT
IN [1..length)
DO
char: CHAR = Rope.Fetch[raw, i];
SELECT char
FROM
',, ': => { host ← Rope.Substr[raw, 1, i-1]; EXIT; };
ENDCASE => NULL;
REPEAT
FINISHED =>
SMTPSupport.Log[important, "Invalid syntax in return path: ", raw];
ENDLOOP; }
ELSE {
--Foo@Bar
FOR i:
INT
DECREASING
IN [0..length)
DO
c: CHAR = Rope.Fetch[raw, i];
IF c = '@ THEN { host ← Rope.Substr[raw, i + 1, (length-i-1)]; EXIT; };
ENDLOOP; };
SELECT
TRUE
FROM
(host = NIL) => NULL;
~CheckHostName[host] =>
SMTPSupport.Log[important, "Invalid character in first return return host: ", raw];
IPName.LoadCacheFromName[host,
TRUE,
FALSE] = bogus => {
SMTPSupport.Log[important, "BOGUS host name in return path: ", host]; };
ENDCASE => NULL;
SELECT
TRUE
FROM
Rope.Find[raw, ","] # -1 =>
@A,@B:User@Host => comma would be bogus in rejection msgs
arpa ← Rope.Cat["\"", raw, "\"@", SMTPControl.xeroxDomain];
ValidDomain[raw] =>
User@Host.ARPA or @Mumble:User@Host.ARPA
arpa ← raw;
raw.Fetch[0] = '@ =>
Can't fixup tail of name: it might be in a different name space
arpa ← Rope.Cat["\"", raw, "\"@", SMTPControl.xeroxDomain];
ENDCASE =>
User@Host (no .ARPA)
Somebody fed us an alias rather than the truth. Normalize it.
BEGIN
length: INT ← Rope.Length[raw];
user, host: ROPE;
FOR i:
INT
DECREASING
IN [0..length)
DO
c: CHAR = raw.Fetch[i];
IF c = '@
THEN {
user ← Rope.Substr[raw, 0, i];
host ← Rope.Substr[raw, i + 1, (length-i-1)];
host ← NormalizeName[host];
IF host.Fetch[0] = '[ THEN host ← Rope.Cat[host, ".ARPA"]; -- [36,1,2,6]
arpa ← Rope.Cat[user, "@", host];
IF ~Tailed[arpa, ".ARPA"]
THEN {
Yetch. Somebody fed us a bogus name. This is needed to keep GV happy.
Rejection msgs from GV will probably not work.
SMTPSupport.Log[important, "Bogus return path: ", raw];
arpa ← Rope.Cat["\"", raw, "\"@", SMTPControl.xeroxDomain]; };
EXIT; };
REPEAT FINISHED => arpa ← raw;
ENDLOOP;
END;
IF arpa # raw THEN SMTPSupport.Log[important, "Return path fixup: ", raw, " => ", arpa];
END;
ValidDomain:
PROC [raw:
ROPE]
RETURNS [
BOOLEAN] = {
FOR list:
LIST
OF Rope.
ROPE ← IPConfig.validDomains, list.rest
UNTIL list =
NIL
DO
domain: Rope.ROPE ← list.first;
IF DotTailed[raw, domain] THEN RETURN[TRUE];
ENDLOOP;
RETURN[FALSE];};
UnBlessReturnPath:
PUBLIC
PROC [raw:
ROPE]
RETURNS [arpa:
ROPE] =
BEGIN
Strip "..."@Xerox.ARPA to be kind to other mailers
(the ones that aren't bright enough to process quotes)
IF raw = NIL THEN RETURN[NIL];
IF Rope.Fetch[raw, 0] #'" THEN RETURN[raw];
IF ~Tailed[raw, SMTPControl.xeroxDomain] THEN RETURN[raw];
RETURN[Rope.Substr[raw, 1, Rope.Length[raw]-3-Rope.Length[SMTPControl.xeroxDomain]]];
END;
ReversePath:
PUBLIC
PROC [gv:
ROPE]
RETURNS [arpa:
ROPE] =
BEGIN
length: INT ← gv.Length[];
FOR i:
INT
DECREASING
IN [0..length)
DO
c: CHAR = gv.Fetch[i];
IF c = '\"
THEN {
-- "Foo" or "Foo".OSBUNorth
IF gv.Fetch[0] # '\" THEN EXIT;
IF i = length-1 THEN gv ← Rope.Substr[gv, 1, length-2]
ELSE gv ← Rope.Cat[Rope.Substr[gv, 1, i-1], Rope.Substr[gv, i+1, length-i-1]];
EXIT; };
IF c = '@
THEN {
glue: ROPE = IF gv.Fetch[0] = '@ THEN "," ELSE ":";
arpa ← Rope.Cat["@", SMTPControl.xeroxDomain, glue, gv];
RETURN; };
ENDLOOP;
gv ← FixupSpaces[gv];
gv ← MaybeAddQuotes[gv];
arpa ← Rope.Cat[gv, "@", SMTPControl.xeroxDomain]; -- Normal GV case
END;
HostAndUser:
PUBLIC
PROC [raw:
ROPE]
RETURNS [host, user:
ROPE] = {
raw ← FixupTail[raw, ".AG"];
raw ← FixupTail[raw, ".ArpaGateway"];
raw ← FixupTail[raw, ".NotArpa"];
raw ← FixupTail[raw, ".ARPA.ARPA"]; -- Hardy 10.0 always adds .ArpaGateway
raw ← FixupTail[raw, ".AG.ARPA"];
FOR list:
LIST
OF Rope.
ROPE ← IPConfig.validDomains, list.rest
UNTIL list =
NIL
DO
domain: Rope.ROPE ← list.first;
rope: Rope.ROPE ← Rope.Cat[".", domain, ".ARPA"];
raw ← TruncateTail[raw, rope];
ENDLOOP;
IF Rope.Find[raw, "].ARPA", 0, FALSE] # -1 THEN raw ← TruncateTail[raw, ".ARPA"];
IF raw.Fetch[0] # '@
THEN {
Hackery to translate user@host.CSNet into user%host@relay.cs.net
Beware: There is similar code in MTTReeOpsImpl
raw ← Redirect[raw, ".BITNET", bitnetGateway];
raw ← Redirect[raw, ".CSNet", csnetGateway];
raw ← Redirect[raw, ".UUCP", uucpGateway];
raw ← Redirect[raw, ".Mailnet", mailnetGateway]; };
IF raw.Fetch[0] # '@
THEN {
Without the next line, mail to ourselves will go around again!!! Good for testing, but..
raw ← StripTail[raw, "@[10.2.0.32].ARPA"];
raw ← StripTail[raw, "@[10.2.0.32].COM"];
raw ← StripTail[raw, "@[10.2.0.32]"];
raw ← StripTail[raw, "@Xerox.COM"];
raw ← StripTail[raw, "@Xerox.ARPA"];
raw ← StripTail[raw, "@Xerox"];
raw ← StripTail[raw, "@PARC.Xerox.COM"];
raw ← StripTail[raw, "@PARC.Xerox"];
raw ← StripTail[raw, "@PARC.ARPA"];
raw ← StripTail[raw, "@PARC"];
raw ← StripTail[raw, "@Parc-Maxc.ARPA"];
raw ← StripTail[raw, "@Parc-Maxc"]; }
ELSE {
Without the next line, mail to ourselves will go around again!!! Good for testing, but..
raw ← StripHead[raw, "@[10.2.0.32].ARPA"];
raw ← StripHead[raw, "@[10.2.0.32].COM"];
raw ← StripHead[raw, "@[10.2.0.32]"];
raw ← StripHead[raw, "@Xerox.COM"];
raw ← StripHead[raw, "@Xerox.ARPA"];
raw ← StripHead[raw, "@Xerox"];
raw ← StripHead[raw, "@PARC.Xerox.COM"];
raw ← StripHead[raw, "@PARC.Xerox"];
raw ← StripHead[raw, "@PARC.ARPA"];
raw ← StripHead[raw, "@PARC"];
raw ← StripHead[raw, "@Parc-Maxc.ARPA"];
raw ← StripHead[raw, "@Parc-Maxc"]; };
raw ← StripQuotes[raw];
[host: host, user: user] ← FindHostName[raw];
IF host =
NIL
THEN {
user ← StripTail[user, ".ARPA"]; -- Hack for testing by sending to Foo.PA.Arpa
user ← ForceRegistry[user];
user ← FixupUnderbars[user]; }; };
Redirect:
PROC [old, domain, relay:
ROPE]
RETURNS [new:
ROPE] =
BEGIN
tail: ROPE ← Rope.Cat[domain, ".ARPA"];
IF Tailed[old, tail] THEN old ← StripTail[old, ".ARPA"];
IF Tailed[old, domain]
THEN {
length: INT;
old ← StripTail[old, domain];
length ← Rope.Length[old];
FOR i:
INT
DECREASING IN [0..length)
DO
IF Rope.Fetch[old, i] = '@
THEN {
name: ROPE ← Rope.Substr[old, 0, i];
name ← StripQuotes[name]; -- "Joe User"@Host.xx
old ← Rope.Cat[name, "%", Rope.Substr[old, i + 1, (length-i-1)]];
old ← MaybeAddQuotes[old]; -- "Joe User%Host.xx"
EXIT; };
ENDLOOP;
old ← Rope.Cat[old, domain, "@", relay]; };
RETURN[old];
END;
StripQuotes:
PROC [old:
ROPE]
RETURNS [new:
ROPE] = {
length: INT ← old.Length[];
new ← old;
IF length < 2 THEN RETURN;
IF old.Fetch[0] # '\" THEN RETURN;
SELECT TRUE FROM
old.Fetch[length-1] = '\" => new ← old.Substr[1, length-1-1];
Tailed[old, "\".ARPA"] => new ← old.Substr[1, length-1-6];
ENDCASE => RETURN;
length ← new.Length[];
FOR i:
INT
IN [0..length)
DO
IF new.Fetch[i] = '\\ THEN EXIT;
REPEAT FINISHED => RETURN; -- No \ inside the string
ENDLOOP;
BEGIN
quoteSeen: BOOLEAN ← FALSE;
text: REF TEXT ← RefText.ObtainScratch[length];
FOR i:
INT
IN [0..length)
DO
c: CHAR = new.Fetch[i];
IF c = '\\ AND ~quoteSeen THEN { quoteSeen ← TRUE; LOOP; };
text ← RefText.AppendChar[text, c];
quoteSeen ← FALSE;
ENDLOOP;
new ← Rope.FromRefText[text];
RefText.ReleaseScratch[text];
END; };
MaybeAddQuotes:
PROC [old:
ROPE]
RETURNS [new:
ROPE] =
BEGIN
See pg 10 of RFC 822. An Atom is anything except specials, SPACE, and CTLs.
This won't do the right things with Foo..bar
length: INT ← old.Length[];
new ← old;
IF Rope.IsEmpty[new] THEN RETURN;
IF Rope.Fetch[new, 0] = '" THEN RETURN; -- Assume already quoted correctly
FOR i:
INT
IN [0..length)
DO
SELECT Rope.Fetch[new, i]
FROM
> 177C => EXIT; -- Funny characters. What should happen to these??
'(, '), '<, '>, '@, '<, ';, ':, '\\, '", '[, '] => EXIT; -- Specials EXCEPT PERIOD!
' => EXIT; -- Space
< 040C => EXIT; -- CTL
ENDCASE => NULL; -- Includes underbar
REPEAT FINISHED => RETURN; -- Nothing fancy inside the string
ENDLOOP;
new ← Rope.Cat["\"", old, "\""];
END;
FixupTail:
PROC [old, tail:
ROPE]
RETURNS [new:
ROPE] = {
new ← old;
IF Tailed[old, tail]
THEN {
new ← StripTail[old, tail];
new ← Rope.Concat[new, ".ARPA"]; }; };
TruncateTail:
PROC [old, tail:
ROPE]
RETURNS [new:
ROPE] = {
new ← old;
IF Tailed[old, tail] THEN new ← StripTail[old, ".ARPA"]; };
Tailed:
PROC [body, tail:
ROPE]
RETURNS [match:
BOOL] = {
bodyLength: INT = body.Length[];
tailLength: INT = tail.Length[];
back: ROPE;
IF bodyLength <= tailLength THEN RETURN[FALSE];
back ← Rope.Substr[body, bodyLength-tailLength, tailLength];
IF Rope.Equal[back, tail, FALSE] THEN RETURN[TRUE];
RETURN[FALSE]; };
DotTailed:
PROC [body, tail:
ROPE]
RETURNS [match:
BOOL] = {
IF ~Tailed[body, tail] THEN RETURN[FALSE];
IF Rope.Fetch[body, Rope.Length[body]-Rope.Length[tail]-1] # '. THEN RETURN[FALSE];
RETURN[TRUE]; };
StripTail:
PROC [body, tail:
ROPE]
RETURNS [new:
ROPE] = {
bodyLength: INT = body.Length[];
tailLength: INT = tail.Length[];
IF ~Tailed[body, tail] THEN RETURN[body];
RETURN[Rope.Substr[body, 0, bodyLength - tailLength]]};
Headed:
PROC [body, head:
ROPE]
RETURNS [match:
BOOL] = {
bodyLength: INT = body.Length[];
headLength: INT = head.Length[];
char: CHAR;
IF bodyLength <= headLength+1 THEN RETURN[FALSE];
char ← Rope.Fetch[body, headLength];
IF char = ',
OR char = ':
THEN {
front: ROPE ← Rope.Substr[body, 0, headLength];
IF Rope.Equal[front, head, FALSE] THEN RETURN[TRUE]; };
RETURN[FALSE]; };
StripHead:
PROC [body, head:
ROPE]
RETURNS [new:
ROPE] = {
bodyLength: INT = body.Length[];
headLength: INT = head.Length[];
IF ~Headed[body, head] THEN RETURN[body];
NB: Remove following , or : too
RETURN[Rope.Substr[body, headLength+1, bodyLength - headLength -1]]};
FindHostName:
PROC [raw:
ROPE]
RETURNS [host, user:
ROPE] = {
length: INT ← raw.Length[];
user ← raw;
IF Rope.Fetch[raw, 0] = '@
THEN {
-- @Foo:X@Y case
FOR i:
INT
IN [1..length)
DO
char: CHAR = Rope.Fetch[raw, i];
SELECT char
FROM
',, ': => {
host ← Rope.Substr[raw, 1, i-1];
IF ~CheckHostName[host] THEN RETURN["Invalid character in host name", raw];
host ← NormalizeName[host];
user ← Rope.Substr[raw, i + 1, (length-i-1)];
No quote checking on this path
user ← Rope.Cat["@", host, Rope.FromChar[char], user];
RETURN; };
ENDCASE => NULL;
REPEAT FINISHED => RETURN["Invalid Syntax", raw];
ENDLOOP; };
FOR i:
INT
DECREASING
IN [0..length)
DO
c: CHAR = raw.Fetch[i];
IF c = '\" THEN EXIT;
IF c = '@
THEN {
--Foo@Bar
user ← Rope.Substr[raw, 0, i];
host ← Rope.Substr[raw, i + 1, (length-i-1)];
IF ~CheckHostName[host] THEN RETURN["Invalid character in host name", raw];
host ← NormalizeName[host];
user ← MaybeAddQuotes[user]; -- "Foo Foo"@Bar
user ← Rope.Cat[user, "@", host];
RETURN; };
ENDLOOP;
FOR i:
INT
DECREASING
IN [0..length)
DO
c: CHAR = raw.Fetch[i];
IF c = '\" THEN EXIT;
IF c = '%
THEN {
-- Hackery: foo%bar
user ← Rope.Substr[raw, 0, i];
host ← Rope.Substr[raw, i + 1, (length-i-1)];
IF ~CheckHostName[host] THEN RETURN["Invalid character in host name", raw];
host ← NormalizeName[host];
user ← MaybeAddQuotes[user];
user ← Rope.Cat[user, "@", host];
RETURN; };
ENDLOOP;
host ← NIL; }; -- No @, Must be GV
CheckHostName:
PROC [host:
ROPE]
RETURNS [ok:
BOOLEAN] =
BEGIN
length: INT ← Rope.Length[host];
FOR i:
INT
IN [1..length)
DO
char: CHAR = Rope.Fetch[host, i];
SELECT char
FROM
'(, '), '<, '>, '@, '<, ';, ':, '\\, '" => RETURN[FALSE]; -- Specials EXCEPT PERIOD and []!
' => RETURN[FALSE]; -- Space
< 040C => RETURN[FALSE]; -- CTL
ENDCASE => NULL;
ENDLOOP;
RETURN[TRUE];
END;
NormalizeName:
PROC [raw:
ROPE]
RETURNS [host:
ROPE] =
BEGIN
host ← IPName.NormalizeName[raw];
IF host # NIL THEN RETURN;
RETURN[raw];
END;
ForceRegistry:
PROC [raw:
ROPE]
RETURNS [user:
ROPE] =
BEGIN
length: INT ← raw.Length[];
user ← raw;
FOR i:
INT
DECREASING IN [0..length)
DO
IF raw.Fetch[i] = '. THEN RETURN;
ENDLOOP;
user ← Rope.Concat[raw, SMTPControl.defaultRegistry];
END;
FixupSpaces:
PROC [raw:
ROPE]
RETURNS [user:
ROPE] =
BEGIN
length: INT ← raw.Length[];
user ← raw;
FOR i:
INT IN [0..length)
DO
IF raw.Fetch[i] = ' THEN EXIT;
REPEAT FINISHED => RETURN;
ENDLOOP;
user ← Rope.Translate[base: user, translator: SpaceToUnderbar];
END;
FixupUnderbars:
PROC [raw:
ROPE]
RETURNS [user:
ROPE] =
BEGIN
length: INT ← raw.Length[];
name, registry: ROPE;
user ← raw;
FOR i:
INT IN [0..length)
DO
IF raw.Fetch[i] = '← THEN EXIT;
REPEAT FINISHED => RETURN;
ENDLOOP;
FOR i:
INT
DECREASING IN [0..length)
DO
IF raw.Fetch[i]
= '. THEN {
name ← Rope.Substr[raw, 0, i];
registry ← Rope.Substr[raw, i, (length-i)]; -- Registry includes the dot
EXIT; };
REPEAT FINISHED => name ← user; -- No registry (?)
ENDLOOP;
name ← Rope.Translate[base: name, translator: UnderbarToSpace];
user ← Rope.Cat["\"", name, "\"", registry];
END;
UnderbarToSpace:
PROC [old:
CHAR]
RETURNS [new:
CHAR] =
BEGIN
IF old = '← THEN RETURN[' ] ELSE RETURN[old];
END;
SpaceToUnderbar:
PROC [old:
CHAR]
RETURNS [new:
CHAR] =
BEGIN
IF old = ' THEN RETURN['←] ELSE RETURN[old];
END;
END.