access.mx raw

   1  package protocol
   2  
   3  import (
   4  	"git.smesh.lol/nostr/pkg/lol/errorf"
   5  )
   6  
   7  // FilterTier is the parsed d value of an access filter. The three forms are
   8  // fan:<asset-address> (the owner's direct buyers), retail:<asset-address> (a
   9  // retailer's per-asset buyers) and sub:<retailer-pubkey>:<channel-d> (a
  10  // retailer's active subscribers). The asset address is itself a full
  11  // kind:pubkey:d string, so the d value contains colons by design.
  12  type FilterTier struct {
  13  	Tier     string
  14  	Asset    *Address // fan, retail
  15  	Retailer string   // sub
  16  	Channel  string   // sub
  17  }
  18  
  19  func (t *FilterTier) String() (s string) {
  20  	if t == nil {
  21  		return ""
  22  	}
  23  	switch t.Tier {
  24  	case TierSub:
  25  		return TierSub | ":" | t.Retailer | ":" | t.Channel
  26  	}
  27  	if t.Asset == nil {
  28  		return t.Tier | ":"
  29  	}
  30  	return t.Tier | ":" | t.Asset.String()
  31  }
  32  
  33  // ParseFilterD parses the d value of an access filter.
  34  func ParseFilterD(d string) (t *FilterTier, err error) {
  35  	i := indexByte(d, ':')
  36  	if i < 0 {
  37  		err = errorf.E("filter d %q has no tier", d)
  38  		return
  39  	}
  40  	tier := d[:i]
  41  	if !AccessTier(tier) {
  42  		err = errorf.E("filter d %q: %q is not a tier", d, tier)
  43  		return
  44  	}
  45  	rest := d[i+1:]
  46  	if tier == TierSub {
  47  		j := indexByte(rest, ':')
  48  		if j < 0 {
  49  			err = errorf.E("filter d %q: a subscription filter needs a retailer and a channel", d)
  50  			return
  51  		}
  52  		retailer := rest[:j]
  53  		if !IsHex64(retailer) {
  54  			err = errorf.E("filter d %q: retailer pubkey is not 64 hex characters", d)
  55  			return
  56  		}
  57  		if rest[j+1:] == "" {
  58  			err = errorf.E("filter d %q: empty channel", d)
  59  			return
  60  		}
  61  		t = &FilterTier{Tier: TierSub, Retailer: retailer, Channel: rest[j+1:]}
  62  		return
  63  	}
  64  	asset, aerr := ParseAddress(rest)
  65  	if aerr != nil {
  66  		err = errorf.E("filter d %q: %s", d, aerr.Error())
  67  		return
  68  	}
  69  	t = &FilterTier{Tier: tier, Asset: asset}
  70  	return
  71  }
  72  
  73  // FanFilterD is the d value an owner publishes for an asset.
  74  func FanFilterD(asset *Address) (s string) {
  75  	return TierFan | ":" | asset.String()
  76  }
  77  
  78  // RetailFilterD is the d value a retailer publishes for an asset it sold.
  79  func RetailFilterD(asset *Address) (s string) {
  80  	return TierRetail | ":" | asset.String()
  81  }
  82  
  83  // SubFilterD is the d value a retailer publishes for a subscription channel.
  84  func SubFilterD(retailer, channel string) (s string) {
  85  	return TierSub | ":" | retailer | ":" | channel
  86  }
  87  
  88  // AccessFilter is the payload of a kind 32217 event: the signed probabilistic
  89  // entitlement set for one asset and tier. The set itself lives in a Blossom
  90  // blob named by x; the event carries the parameters that bind the bytes.
  91  type AccessFilter struct {
  92  	D          string // <tier>:<asset-address>, or the subscription form
  93  	Filter     string // bloom
  94  	Fp         string // false-positive rate, <= 1e-6, spec minimum 1e-9
  95  	N          string // declared element count
  96  	K          string // hash-function count, round(-log2(fp))
  97  	X          string // sha256 of the filter blob
  98  	Server     string // Blossom hint; falls back to the owner's server list
  99  	Mints      []string // accepted Cashu mints; "lightning" for a direct leg
 100  	Retailers  []string // authorized retailer pubkeys
 101  	Expiration string   // unix seconds; absent means permanent
 102  	Catalog    []string // subscription coverage: covered asset addresses
 103  	Content    string
 104  	Extra      [][]string
 105  }
 106  
 107  func (f *AccessFilter) Tags() (tags [][]string) {
 108  	tags = appendKV(nil, TagD, f.D)
 109  	tags = appendKV(tags, TagFilter, f.Filter)
 110  	tags = appendKV(tags, TagFp, f.Fp)
 111  	tags = appendKV(tags, TagN, f.N)
 112  	tags = appendKV(tags, TagK, f.K)
 113  	tags = appendKV(tags, TagX, f.X)
 114  	tags = appendKV(tags, TagServer, f.Server)
 115  	for i := range f.Mints {
 116  		tags = appendKV(tags, TagMint, f.Mints[i])
 117  	}
 118  	for i := range f.Retailers {
 119  		tags = appendKV(tags, TagRetailer, f.Retailers[i])
 120  	}
 121  	tags = appendKV(tags, TagExpiration, f.Expiration)
 122  	for i := range f.Catalog {
 123  		tags = appendKV(tags, TagCatalog, f.Catalog[i])
 124  	}
 125  	tags = appendTags(tags, f.Extra)
 126  	return
 127  }
 128  
 129  func ParseAccessFilter(tags [][]string, content string) (f *AccessFilter, err error) {
 130  	f = &AccessFilter{Content: content}
 131  	for i := range tags {
 132  		tg := tags[i]
 133  		if len(tg) < 1 {
 134  			continue
 135  		}
 136  		switch tg[0] {
 137  		case TagD:
 138  			f.D = elem(tg, 1)
 139  		case TagFilter:
 140  			f.Filter = elem(tg, 1)
 141  		case TagFp:
 142  			f.Fp = elem(tg, 1)
 143  		case TagN:
 144  			f.N = elem(tg, 1)
 145  		case TagK:
 146  			f.K = elem(tg, 1)
 147  		case TagX:
 148  			f.X = elem(tg, 1)
 149  		case TagServer:
 150  			f.Server = elem(tg, 1)
 151  		case TagMint:
 152  			f.Mints = push(f.Mints, elem(tg, 1))
 153  		case TagRetailer:
 154  			f.Retailers = push(f.Retailers, elem(tg, 1))
 155  		case TagExpiration:
 156  			f.Expiration = elem(tg, 1)
 157  		case TagCatalog:
 158  			// On a filter, catalog carries covered asset addresses; the same
 159  			// tag name on an edition carries a release catalog number. The
 160  			// kind disambiguates.
 161  			for j := int32(1); j < int32(len(tg)); j++ {
 162  				f.Catalog = push(f.Catalog, tg[j])
 163  			}
 164  		default:
 165  			f.Extra = appendTag(f.Extra, tg)
 166  		}
 167  	}
 168  	return
 169  }
 170  
 171  func (f *AccessFilter) Validate() (err error) {
 172  	if f == nil {
 173  		err = errorf.E("nil access filter")
 174  		return
 175  	}
 176  	if _, terr := ParseFilterD(f.D); terr != nil {
 177  		err = terr
 178  		return
 179  	}
 180  	if f.Filter == "" {
 181  		err = errorf.E("access filter: no filter tag")
 182  		return
 183  	}
 184  	if f.Filter != FilterBloom {
 185  		// An unknown structure must fail closed at the host, which needs the
 186  		// value it saw; validation only says it is not one we can read.
 187  		err = errorf.E("access filter: unknown filter structure %q", f.Filter)
 188  		return
 189  	}
 190  	if !IsHex64(f.X) {
 191  		err = errorf.E("access filter: blob hash %q is not 64 hex characters", f.X)
 192  		return
 193  	}
 194  	if f.Fp == "" {
 195  		err = errorf.E("access filter: no false-positive rate")
 196  		return
 197  	}
 198  	if f.N == "" {
 199  		err = errorf.E("access filter: no element count")
 200  		return
 201  	}
 202  	if f.K == "" {
 203  		err = errorf.E("access filter: no hash-function count")
 204  		return
 205  	}
 206  	for i := range f.Retailers {
 207  		if !IsHex64(f.Retailers[i]) {
 208  			err = errorf.E("access filter: retailer %d pubkey is not 64 hex characters", i)
 209  			return
 210  		}
 211  	}
 212  	return
 213  }
 214  
 215  // Warnings reports the subscription coverage gap the spec leaves open: a sub
 216  // filter must carry catalog tags or resolve to an addressable list.
 217  func (f *AccessFilter) Warnings() (w []string) {
 218  	tier, err := ParseFilterD(f.D)
 219  	if err != nil {
 220  		return
 221  	}
 222  	if tier.Tier == TierSub && len(f.Catalog) == 0 {
 223  		w = push(w, "subscription filter with no catalog coverage")
 224  	}
 225  	if f.Expiration == "" {
 226  		w = push(w, "no expiration: the filter is permanent")
 227  	}
 228  	return
 229  }
 230  
 231  // PurchaseOrder is the payload of a kind 3222 event: a buyer's order carrying
 232  // intent and payment proof in one message. It is never published - it exists
 233  // only inside a NIP-59 wrap addressed to the seller.
 234  type PurchaseOrder struct {
 235  	Asset   *Address
 236  	Seller  string
 237  	Tier    string
 238  	Price   *Price
 239  	Payment string // cashu or lightning
 240  	Mint    string // cashu leg
 241  	Token   string // cashuB..., P2PK-locked to the seller
 242  	Channel string // subscription orders
 243  	Period  string // subscription orders
 244  	Content string
 245  	Extra   [][]string
 246  }
 247  
 248  func (o *PurchaseOrder) Tags() (tags [][]string) {
 249  	tags = appendTag(nil, o.Asset.Tag())
 250  	tags = appendKV(tags, TagP, o.Seller)
 251  	tags = appendKV(tags, TagTier, o.Tier)
 252  	tags = appendTag(tags, o.Price.Tag())
 253  	tags = appendKV(tags, TagPayment, o.Payment)
 254  	tags = appendKV(tags, TagMint, o.Mint)
 255  	tags = appendKV(tags, TagToken, o.Token)
 256  	tags = appendKV(tags, TagChannel, o.Channel)
 257  	tags = appendKV(tags, TagPeriod, o.Period)
 258  	tags = appendTags(tags, o.Extra)
 259  	return
 260  }
 261  
 262  func ParsePurchaseOrder(tags [][]string, content string) (o *PurchaseOrder, err error) {
 263  	o = &PurchaseOrder{Content: content}
 264  	for i := range tags {
 265  		tg := tags[i]
 266  		if len(tg) < 1 {
 267  			continue
 268  		}
 269  		switch tg[0] {
 270  		case TagA:
 271  			a, aerr := AddressFromTag(tg)
 272  			if aerr != nil {
 273  				err = aerr
 274  				return
 275  			}
 276  			o.Asset = a
 277  		case TagP:
 278  			o.Seller = elem(tg, 1)
 279  		case TagTier:
 280  			o.Tier = elem(tg, 1)
 281  		case TagPrice:
 282  			o.Price = parsePrice(tg)
 283  		case TagPayment:
 284  			o.Payment = elem(tg, 1)
 285  		case TagMint:
 286  			o.Mint = elem(tg, 1)
 287  		case TagToken:
 288  			o.Token = elem(tg, 1)
 289  		case TagChannel:
 290  			o.Channel = elem(tg, 1)
 291  		case TagPeriod:
 292  			o.Period = elem(tg, 1)
 293  		default:
 294  			o.Extra = appendTag(o.Extra, tg)
 295  		}
 296  	}
 297  	return
 298  }
 299  
 300  func (o *PurchaseOrder) Validate() (err error) {
 301  	if o == nil {
 302  		err = errorf.E("nil purchase order")
 303  		return
 304  	}
 305  	if o.Asset == nil {
 306  		err = errorf.E("purchase order: no asset address")
 307  		return
 308  	}
 309  	if !IsHex64(o.Seller) {
 310  		err = errorf.E("purchase order: seller pubkey is not 64 hex characters")
 311  		return
 312  	}
 313  	if o.Tier != "" && !AccessTier(o.Tier) {
 314  		err = errorf.E("purchase order: %q is not a tier", o.Tier)
 315  		return
 316  	}
 317  	switch o.Payment {
 318  	case "":
 319  		err = errorf.E("purchase order: no payment leg")
 320  		return
 321  	case PaymentCashu:
 322  		if o.Token == "" || o.Mint == "" {
 323  			err = errorf.E("purchase order: a cashu leg needs a mint and a token")
 324  			return
 325  		}
 326  	case PaymentLightning:
 327  		// The proof is a NIP-57 zap receipt found separately; the order
 328  		// itself carries only the leg name.
 329  	}
 330  	if o.Tier == TierSub && (o.Channel == "" || o.Period == "") {
 331  		err = errorf.E("purchase order: a subscription needs a channel and a period")
 332  		return
 333  	}
 334  	return
 335  }
 336  
 337  // DeliveryReceipt is the payload of a kind 32218 event: the host-signed deed
 338  // for one serve. It is never published; one wrap goes to the buyer and one to
 339  // the publisher, and the publisher's archive is the durable record.
 340  type DeliveryReceipt struct {
 341  	Asset      *Address
 342  	Buyer      string
 343  	Blob       string
 344  	Server     string
 345  	Mode       string // download or stream
 346  	Start      string
 347  	End        string
 348  	Bytes      string
 349  	AmountSats string
 350  	Paid       []string // token hashes spent at this serve
 351  	Content    string
 352  	Extra      [][]string
 353  }
 354  
 355  func (r *DeliveryReceipt) Tags() (tags [][]string) {
 356  	tags = appendTag(nil, r.Asset.Tag())
 357  	tags = appendKV(tags, TagP, r.Buyer)
 358  	tags = appendKV(tags, TagX, r.Blob)
 359  	tags = appendKV(tags, TagServer, r.Server)
 360  	tags = appendKV(tags, TagMode, r.Mode)
 361  	tags = appendKV(tags, TagStart, r.Start)
 362  	tags = appendKV(tags, TagEnd, r.End)
 363  	tags = appendKV(tags, TagBytes, r.Bytes)
 364  	tags = appendKV(tags, TagAmountSats, r.AmountSats)
 365  	for i := range r.Paid {
 366  		tags = appendKV(tags, TagPaid, r.Paid[i])
 367  	}
 368  	tags = appendTags(tags, r.Extra)
 369  	return
 370  }
 371  
 372  func ParseDeliveryReceipt(tags [][]string, content string) (r *DeliveryReceipt, err error) {
 373  	r = &DeliveryReceipt{Content: content}
 374  	for i := range tags {
 375  		tg := tags[i]
 376  		if len(tg) < 1 {
 377  			continue
 378  		}
 379  		switch tg[0] {
 380  		case TagA:
 381  			a, aerr := AddressFromTag(tg)
 382  			if aerr != nil {
 383  				err = aerr
 384  				return
 385  			}
 386  			r.Asset = a
 387  		case TagP:
 388  			r.Buyer = elem(tg, 1)
 389  		case TagX:
 390  			r.Blob = elem(tg, 1)
 391  		case TagServer:
 392  			r.Server = elem(tg, 1)
 393  		case TagMode:
 394  			r.Mode = elem(tg, 1)
 395  		case TagStart:
 396  			r.Start = elem(tg, 1)
 397  		case TagEnd:
 398  			r.End = elem(tg, 1)
 399  		case TagBytes:
 400  			r.Bytes = elem(tg, 1)
 401  		case TagAmountSats:
 402  			r.AmountSats = elem(tg, 1)
 403  		case TagPaid:
 404  			r.Paid = push(r.Paid, elem(tg, 1))
 405  		default:
 406  			r.Extra = appendTag(r.Extra, tg)
 407  		}
 408  	}
 409  	return
 410  }
 411  
 412  func (r *DeliveryReceipt) Validate() (err error) {
 413  	if r == nil {
 414  		err = errorf.E("nil delivery receipt")
 415  		return
 416  	}
 417  	if r.Asset == nil {
 418  		err = errorf.E("delivery receipt: no asset address")
 419  		return
 420  	}
 421  	if !IsHex64(r.Buyer) {
 422  		err = errorf.E("delivery receipt: buyer pubkey is not 64 hex characters")
 423  		return
 424  	}
 425  	if !IsHex64(r.Blob) {
 426  		err = errorf.E("delivery receipt: blob hash is not 64 hex characters")
 427  		return
 428  	}
 429  	switch r.Mode {
 430  	case ModeDownload, ModeStream:
 431  	case "":
 432  		err = errorf.E("delivery receipt: no mode")
 433  		return
 434  	default:
 435  		err = errorf.E("delivery receipt: %q is not a mode", r.Mode)
 436  		return
 437  	}
 438  	if r.AmountSats == "" {
 439  		err = errorf.E("delivery receipt: no amount")
 440  		return
 441  	}
 442  	return
 443  }
 444  
 445  // TokenHashesUsed reports the token hashes a receipt claims, so a host can
 446  // link a deed to a payment without the token itself ever being published.
 447  func (r *DeliveryReceipt) TokenHashesUsed() (n int32) {
 448  	return int32(len(r.Paid))
 449  }
 450  
 451  // HostAggregate is the payload of a kind 32219 event: a serving host's signed
 452  // totals for one asset and period. Immutable: a correction is a new event with
 453  // a revised period.
 454  type HostAggregate struct {
 455  	Asset     *Address
 456  	Start     string
 457  	End       string
 458  	Plays     string
 459  	Downloads string
 460  	Bytes     string
 461  	Sats      string
 462  	Listeners string
 463  	Approx    bool // listeners comes from a probabilistic filter and is an estimate
 464  	Content   string
 465  	Extra     [][]string
 466  }
 467  
 468  func (a *HostAggregate) Tags() (tags [][]string) {
 469  	tags = appendTag(nil, a.Asset.Tag())
 470  	tags = appendKV(tags, TagStart, a.Start)
 471  	tags = appendKV(tags, TagEnd, a.End)
 472  	tags = appendKV(tags, TagPlays, a.Plays)
 473  	tags = appendKV(tags, TagDownloads, a.Downloads)
 474  	tags = appendKV(tags, TagBytes, a.Bytes)
 475  	tags = appendKV(tags, TagSats, a.Sats)
 476  	if a.Approx {
 477  		t := []string{:3}
 478  		t[0] = TagListeners
 479  		t[1] = a.Listeners
 480  		t[2] = Approx
 481  		tags = appendTag(tags, t)
 482  	} else {
 483  		tags = appendKV(tags, TagListeners, a.Listeners)
 484  	}
 485  	tags = appendTags(tags, a.Extra)
 486  	return
 487  }
 488  
 489  func ParseHostAggregate(tags [][]string, content string) (a *HostAggregate, err error) {
 490  	a = &HostAggregate{Content: content}
 491  	for i := range tags {
 492  		tg := tags[i]
 493  		if len(tg) < 1 {
 494  			continue
 495  		}
 496  		switch tg[0] {
 497  		case TagA:
 498  			addr, aerr := AddressFromTag(tg)
 499  			if aerr != nil {
 500  				err = aerr
 501  				return
 502  			}
 503  			a.Asset = addr
 504  		case TagStart:
 505  			a.Start = elem(tg, 1)
 506  		case TagEnd:
 507  			a.End = elem(tg, 1)
 508  		case TagPlays:
 509  			a.Plays = elem(tg, 1)
 510  		case TagDownloads:
 511  			a.Downloads = elem(tg, 1)
 512  		case TagBytes:
 513  			a.Bytes = elem(tg, 1)
 514  		case TagSats:
 515  			a.Sats = elem(tg, 1)
 516  		case TagListeners:
 517  			a.Listeners = elem(tg, 1)
 518  			if elem(tg, 2) == Approx {
 519  				a.Approx = true
 520  			}
 521  		default:
 522  			a.Extra = appendTag(a.Extra, tg)
 523  		}
 524  	}
 525  	return
 526  }
 527  
 528  func (a *HostAggregate) Validate() (err error) {
 529  	if a == nil {
 530  		err = errorf.E("nil host aggregate")
 531  		return
 532  	}
 533  	if a.Asset == nil {
 534  		err = errorf.E("host aggregate: no asset address")
 535  		return
 536  	}
 537  	if a.Start == "" || a.End == "" {
 538  		err = errorf.E("host aggregate: no period")
 539  		return
 540  	}
 541  	if a.Listeners != "" && !a.Approx {
 542  		// The spec says the estimate must say so; a bare count reads as exact.
 543  		err = errorf.E("host aggregate: listeners must be marked approx")
 544  		return
 545  	}
 546  	return
 547  }
 548  
 549  // AggregateRef is an `e` tag on a publisher rollup: the referenced host
 550  // aggregate, plus the host that signed it. The relay slot is written empty so
 551  // the host pubkey stays at index 3.
 552  type AggregateRef struct {
 553  	ID   string
 554  	Host string
 555  }
 556  
 557  func (r *AggregateRef) Tag() (t []string) {
 558  	if r == nil {
 559  		return nil
 560  	}
 561  	t = []string{:4}
 562  	t[0] = TagE
 563  	t[1] = r.ID
 564  	t[2] = ""
 565  	t[3] = r.Host
 566  	return
 567  }
 568  
 569  // PublisherRollup is the payload of a kind 32220 event: the publisher's signed
 570  // pointer-set over host aggregates plus its own receipt-archive totals. The
 571  // leaves stay host-signed, so a rollup never restates a host's numbers.
 572  type PublisherRollup struct {
 573  	Asset      *Address
 574  	Start      string
 575  	End        string
 576  	Aggregates []AggregateRef
 577  	Receipts   string
 578  	Plays      string
 579  	Downloads  string
 580  	Bytes      string
 581  	Sats       string
 582  	Content    string
 583  	Extra      [][]string
 584  }
 585  
 586  func (r *PublisherRollup) Tags() (tags [][]string) {
 587  	tags = appendTag(nil, r.Asset.Tag())
 588  	tags = appendKV(tags, TagStart, r.Start)
 589  	tags = appendKV(tags, TagEnd, r.End)
 590  	for i := range r.Aggregates {
 591  		tags = appendTag(tags, r.Aggregates[i].Tag())
 592  	}
 593  	tags = appendKV(tags, TagReceipts, r.Receipts)
 594  	tags = appendKV(tags, TagPlays, r.Plays)
 595  	tags = appendKV(tags, TagDownloads, r.Downloads)
 596  	tags = appendKV(tags, TagBytes, r.Bytes)
 597  	tags = appendKV(tags, TagSats, r.Sats)
 598  	tags = appendTags(tags, r.Extra)
 599  	return
 600  }
 601  
 602  func ParsePublisherRollup(tags [][]string, content string) (r *PublisherRollup, err error) {
 603  	r = &PublisherRollup{Content: content}
 604  	for i := range tags {
 605  		tg := tags[i]
 606  		if len(tg) < 1 {
 607  			continue
 608  		}
 609  		switch tg[0] {
 610  		case TagA:
 611  			a, aerr := AddressFromTag(tg)
 612  			if aerr != nil {
 613  				err = aerr
 614  				return
 615  			}
 616  			r.Asset = a
 617  		case TagStart:
 618  			r.Start = elem(tg, 1)
 619  		case TagEnd:
 620  			r.End = elem(tg, 1)
 621  		case TagE:
 622  			r.Aggregates = push(r.Aggregates, AggregateRef{ID: elem(tg, 1), Host: elem(tg, 3)})
 623  		case TagReceipts:
 624  			r.Receipts = elem(tg, 1)
 625  		case TagPlays:
 626  			r.Plays = elem(tg, 1)
 627  		case TagDownloads:
 628  			r.Downloads = elem(tg, 1)
 629  		case TagBytes:
 630  			r.Bytes = elem(tg, 1)
 631  		case TagSats:
 632  			r.Sats = elem(tg, 1)
 633  		default:
 634  			r.Extra = appendTag(r.Extra, tg)
 635  		}
 636  	}
 637  	return
 638  }
 639  
 640  func (r *PublisherRollup) Validate() (err error) {
 641  	if r == nil {
 642  		err = errorf.E("nil publisher rollup")
 643  		return
 644  	}
 645  	if r.Asset == nil {
 646  		err = errorf.E("publisher rollup: no asset address")
 647  		return
 648  	}
 649  	if r.Start == "" || r.End == "" {
 650  		err = errorf.E("publisher rollup: no period")
 651  		return
 652  	}
 653  	for i := range r.Aggregates {
 654  		if !IsHex64(r.Aggregates[i].ID) {
 655  			err = errorf.E("publisher rollup: aggregate %d id is not 64 hex characters", i)
 656  			return
 657  		}
 658  		if !IsHex64(r.Aggregates[i].Host) {
 659  			err = errorf.E("publisher rollup: aggregate %d host pubkey is not 64 hex characters", i)
 660  			return
 661  		}
 662  	}
 663  	return
 664  }
 665  
 666  // indexByte returns the index of the first c in s, or -1.
 667  func indexByte(s string, c byte) (i int32) {
 668  	for j := int32(0); j < int32(len(s)); j++ {
 669  		if s[j] == c {
 670  			return j
 671  		}
 672  	}
 673  	return -1
 674  }
 675