From ba80173b6a4e4a2d03459f264907403eb698d789 Mon Sep 17 00:00:00 2001 From: Lime Date: Wed, 10 Aug 2016 11:27:32 +0800 Subject: [PATCH] add license && golint --- LICENSE | 21 +++++ dht.go | 7 +- krpc.go | 44 +++++----- peerwire.go | 27 +++--- routingtable.go | 184 ++++++++++++++++++++-------------------- sample/spider/spider.go | 18 ++-- 6 files changed, 165 insertions(+), 136 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab4304b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Dean Karn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dht.go b/dht.go index 9e5d13c..75abc9d 100644 --- a/dht.go +++ b/dht.go @@ -1,6 +1,5 @@ // Package dht implements the bittorrent dht protocol. For more information // see http://www.bittorrent.org/beps/bep_0005.html. - package dht import ( @@ -18,7 +17,9 @@ var ( ) const ( + // StandardMode follows the standard protocol StandardMode = iota + // CrawlMode for crawling the dht network. CrawlMode ) @@ -132,7 +133,7 @@ func (dht *DHT) IsStandardMode() bool { return dht.Mode == StandardMode } -// IsStandardMode returns whether mode is CrawlMode. +// IsCrawlMode returns whether mode is CrawlMode. func (dht *DHT) IsCrawlMode() bool { return dht.Mode == CrawlMode } @@ -196,7 +197,7 @@ func (dht *DHT) id(target string) string { } // GetPeers returns peers who have announced having infoHash. -func (dht *DHT) GetPeers(infoHash string) ([]*peer, error) { +func (dht *DHT) GetPeers(infoHash string) ([]*Peer, error) { if !dht.Ready { return nil, errors.New("dht not ready") } diff --git a/krpc.go b/krpc.go index f40614d..2542c04 100644 --- a/krpc.go +++ b/krpc.go @@ -68,11 +68,11 @@ func (tm *tokenManager) token(addr *net.UDPAddr) string { } // check returns whether the token is valid. -func (tm *tokenManager) check(addr *net.UDPAddr, token_ string) bool { +func (tm *tokenManager) check(addr *net.UDPAddr, tokenString string) bool { v, ok := tm.Get(addr.IP.String()) tk, _ := v.(token) - return ok && token_ == tk.data + return ok && tokenString == tk.data } // makeQuery returns a query-formed data. @@ -148,8 +148,8 @@ func newTransactionManager(maxCursor uint64, dht *DHT) *transactionManager { } } -// genTransId generates a transaction id and returns it. -func (tm *transactionManager) genTransId() string { +// genTransID generates a transaction id and returns it. +func (tm *transactionManager) genTransID() string { tm.Lock() defer tm.Unlock() @@ -187,8 +187,8 @@ func (tm *transactionManager) insert(trans *transaction) { } // delete removes a transaction from transactionManager. -func (tm *transactionManager) delete(transId string) { - v, ok := tm.transactions.Get(transId) +func (tm *transactionManager) delete(transID string) { + v, ok := tm.transactions.Get(transID) if !ok { return } @@ -224,9 +224,9 @@ func (tm *transactionManager) transaction( return v.(*transaction) } -// getByTransId returns a transaction by transId. -func (tm *transactionManager) getByTransId(transId string) *transaction { - return tm.transaction(transId, 0) +// getByTransID returns a transaction by transID. +func (tm *transactionManager) getByTransID(transID string) *transaction { + return tm.transaction(transID, 0) } // getByIndex returns a transaction by indexed key. @@ -237,9 +237,9 @@ func (tm *transactionManager) getByIndex(index string) *transaction { // transaction gets the proper transaction with whose id is transId and // address is addr. func (tm *transactionManager) filterOne( - transId string, addr *net.UDPAddr) *transaction { + transID string, addr *net.UDPAddr) *transaction { - trans := tm.getByTransId(transId) + trans := tm.getByTransID(transID) if trans == nil || trans.node.addr.String() != addr.String() { return nil } @@ -251,8 +251,8 @@ func (tm *transactionManager) filterOne( // When timeout, it will retry `try - 1` times, which means it will query // `try` times totally. func (tm *transactionManager) query(q *query, try int) { - transId := q.data["t"].(string) - trans := tm.newTransaction(transId, q) + transID := q.data["t"].(string) + trans := tm.newTransaction(transID, q) tm.insert(trans) defer tm.delete(trans.id) @@ -300,7 +300,7 @@ func (tm *transactionManager) sendQuery( return } - data := makeQuery(tm.genTransId(), queryType, a) + data := makeQuery(tm.genTransID(), queryType, a) tm.queryChan <- &query{ node: no, data: data, @@ -465,14 +465,14 @@ func handleRequest(dht *DHT, addr *net.UDPAddr, var nodes string if dht.IsStandardMode() { - targetId := newBitmapFromString(target) + targetID := newBitmapFromString(target) - no, _ := dht.routingTable.GetNodeKBucktById(targetId) + no, _ := dht.routingTable.GetNodeKBucktByID(targetID) if no != nil { nodes = no.CompactNodeInfo() } else { nodes = strings.Join( - dht.routingTable.GetNeighborCompactInfos(targetId, dht.K), + dht.routingTable.GetNeighborCompactInfos(targetID, dht.K), "", ) } @@ -546,8 +546,8 @@ func handleRequest(dht *DHT, addr *net.UDPAddr, return } - if implied_port, ok := a["implied_port"]; ok && - implied_port.(int) != 0 { + if impliedPort, ok := a["implied_port"]; ok && + impliedPort.(int) != 0 { port = addr.Port } @@ -603,13 +603,13 @@ func findOn(dht *DHT, r map[string]interface{}, target *bitmap, return nil } - targetId := target.RawString() + targetID := target.RawString() for _, no := range dht.routingTable.GetNeighbors(target, dht.K) { switch queryType { case findNodeType: - dht.transactionManager.findNode(no, targetId) + dht.transactionManager.findNode(no, targetID) case getPeersType: - dht.transactionManager.getPeers(no, targetId) + dht.transactionManager.getPeers(no, targetID) default: panic("invalid find type") } diff --git a/peerwire.go b/peerwire.go index 2cf8bc5..db90596 100644 --- a/peerwire.go +++ b/peerwire.go @@ -13,16 +13,23 @@ import ( ) const ( + // REQUEST represents request message type REQUEST = iota + // DATA represents data message type DATA + // REJECT represents reject message type REJECT ) const ( - BLOCK = 16384 // 2 ^ 14 - MAX_METADATA_SIZE = BLOCK * 1000 - EXTENDED = 20 - HANDSHAKE = 0 + // BLOCK is 2 ^ 14 + BLOCK = 16384 + // MaxMetadataSize represents the max medata it can accept + MaxMetadataSize = BLOCK * 1000 + // EXTENDED represents it is a extended message + EXTENDED = 20 + // HANDSHAKE represents handshake bit + HANDSHAKE = 0 ) var handshakePrefix = []byte{ @@ -65,7 +72,7 @@ func readMessage(conn *net.TCPConn, data *bytes.Buffer) ( // sendMessage sends data to the connection. func sendMessage(conn *net.TCPConn, data []byte) error { - var length int32 = int32(len(data)) + length := int32(len(data)) buffer := bytes.NewBuffer(nil) binary.Write(buffer, binary.BigEndian, length) @@ -75,11 +82,11 @@ func sendMessage(conn *net.TCPConn, data []byte) error { } // sendHandshake sends handshake message to conn. -func sendHandshake(conn *net.TCPConn, infoHash, peerId []byte) error { +func sendHandshake(conn *net.TCPConn, infoHash, peerID []byte) error { data := make([]byte, 68) copy(data[:28], handshakePrefix) copy(data[28:48], infoHash) - copy(data[48:], peerId) + copy(data[48:], peerID) _, err := conn.Write(data) return err @@ -133,7 +140,7 @@ func getUTMetaSize(data []byte) ( utMetadata = m["ut_metadata"].(int) metadataSize = dict["metadata_size"].(int) - if metadataSize > MAX_METADATA_SIZE { + if metadataSize > MaxMetadataSize { err = errors.New("metadata_size too long") } return @@ -264,7 +271,7 @@ func (wire *Wire) fetchMetadata(r Request) { switch msgType { case EXTENDED: - extendedId, err := data.ReadByte() + extendedID, err := data.ReadByte() if err != nil { return } @@ -274,7 +281,7 @@ func (wire *Wire) fetchMetadata(r Request) { return } - if extendedId == 0 { + if extendedID == 0 { if pieces != nil { return } diff --git a/routingtable.go b/routingtable.go index 61df03b..ff350b6 100644 --- a/routingtable.go +++ b/routingtable.go @@ -62,24 +62,24 @@ func (node *node) CompactNodeInfo() string { }, "") } -// peer represents a peer contact. -type peer struct { - ip net.IP - port int +// Peer represents a peer contact. +type Peer struct { + IP net.IP + Port int token string } // newPeer returns a new peer pointer. -func newPeer(ip net.IP, port int, token string) *peer { - return &peer{ - ip: ip, - port: port, +func newPeer(ip net.IP, port int, token string) *Peer { + return &Peer{ + IP: ip, + Port: port, token: token, } } // newPeerFromCompactIPPortInfo create a peer pointer by compact ip/port info. -func newPeerFromCompactIPPortInfo(compactInfo, token string) (*peer, error) { +func newPeerFromCompactIPPortInfo(compactInfo, token string) (*Peer, error) { ip, port, err := decodeCompactIPPortInfo(compactInfo) if err != nil { return nil, err @@ -88,10 +88,10 @@ func newPeerFromCompactIPPortInfo(compactInfo, token string) (*peer, error) { return newPeer(ip, port, token), nil } -// CompactNodeInfo returns "Compact node info". +// CompactIPPortInfo returns "Compact node info". // See http://www.bittorrent.org/beps/bep_0005.html. -func (p *peer) CompactIPPortInfo() string { - info, _ := encodeCompactIPPortInfo(p.ip, p.port) +func (p *Peer) CompactIPPortInfo() string { + info, _ := encodeCompactIPPortInfo(p.IP, p.Port) return info } @@ -111,7 +111,7 @@ func newPeersManager(dht *DHT) *peersManager { } // Insert adds a peer into peersManager. -func (pm *peersManager) Insert(infoHash string, peer *peer) { +func (pm *peersManager) Insert(infoHash string, peer *Peer) { pm.Lock() if _, ok := pm.table.Get(infoHash); !ok { pm.table.Set(infoHash, newKeyedDeque()) @@ -123,8 +123,8 @@ func (pm *peersManager) Insert(infoHash string, peer *peer) { } // GetPeers returns size-length peers who announces having infoHash. -func (pm *peersManager) GetPeers(infoHash string, size int) []*peer { - peers := make([]*peer, 0, size) +func (pm *peersManager) GetPeers(infoHash string, size int) []*Peer { + peers := make([]*Peer, 0, size) v, ok := pm.table.Get(infoHash) if !ok { @@ -132,7 +132,7 @@ func (pm *peersManager) GetPeers(infoHash string, size int) []*peer { } for e := range v.(*keyedDeque).Iter() { - peers = append(peers, e.Value.(*peer)) + peers = append(peers, e.Value.(*Peer)) } if len(peers) > size { @@ -141,105 +141,105 @@ func (pm *peersManager) GetPeers(infoHash string, size int) []*peer { return peers } -// kBucket represents a k-size bucket. -type kBucket struct { +// kbucket represents a k-size bucket. +type kbucket struct { sync.RWMutex nodes, candidates *keyedDeque lastChanged time.Time prefix *bitmap } -// newKBucket returns a new kBucket pointer. -func newKBucket(prefix *bitmap) *kBucket { - kbucket := &kBucket{ +// newKBucket returns a new kbucket pointer. +func newKBucket(prefix *bitmap) *kbucket { + bucket := &kbucket{ nodes: newKeyedDeque(), candidates: newKeyedDeque(), lastChanged: time.Now(), prefix: prefix, } - return kbucket + return bucket } // LastChanged return the last time when it changes. -func (kbucket *kBucket) LastChanged() time.Time { - kbucket.RLock() - defer kbucket.RUnlock() +func (bucket *kbucket) LastChanged() time.Time { + bucket.RLock() + defer bucket.RUnlock() - return kbucket.lastChanged + return bucket.lastChanged } -// RandomChildID returns a random id that has the same prefix with kbucket. -func (kbucket *kBucket) RandomChildID() string { - prefixLen := kbucket.prefix.Size / 8 +// RandomChildID returns a random id that has the same prefix with bucket. +func (bucket *kbucket) RandomChildID() string { + prefixLen := bucket.prefix.Size / 8 return strings.Join([]string{ - kbucket.prefix.RawString()[:prefixLen], + bucket.prefix.RawString()[:prefixLen], randomString(20 - prefixLen), }, "") } -// UpdateTimestamp update kbucket's last changed time.. -func (kbucket *kBucket) UpdateTimestamp() { - kbucket.Lock() - defer kbucket.Unlock() +// UpdateTimestamp update bucket's last changed time.. +func (bucket *kbucket) UpdateTimestamp() { + bucket.Lock() + defer bucket.Unlock() - kbucket.lastChanged = time.Now() + bucket.lastChanged = time.Now() } -// Insert inserts node to the kbucket. It returns whether the node is new in -// the kbucket. -func (kbucket *kBucket) Insert(no *node, rt *routingTable) bool { - isNew := !kbucket.nodes.HasKey(no.id.RawString()) +// Insert inserts node to the bucket. It returns whether the node is new in +// the bucket. +func (bucket *kbucket) Insert(no *node, rt *routingTable) bool { + isNew := !bucket.nodes.HasKey(no.id.RawString()) - kbucket.nodes.Push(no.id.RawString(), no) + bucket.nodes.Push(no.id.RawString(), no) rt.cachedNodes.Set(no.addr.String(), no) - rt.Touch(kbucket) + rt.Touch(bucket) return isNew } -// Replace removes node, then put kbucket.candidates.Back() to the right -// place of kbucket.nodes. -func (kbucket *kBucket) Replace(no *node, rt *routingTable) { - kbucket.nodes.Delete(no.id.RawString()) +// Replace removes node, then put bucket.candidates.Back() to the right +// place of bucket.nodes. +func (bucket *kbucket) Replace(no *node, rt *routingTable) { + bucket.nodes.Delete(no.id.RawString()) rt.cachedNodes.Delete(no.addr.String()) - rt.Touch(kbucket) + rt.Touch(bucket) - if kbucket.candidates.Len() == 0 { + if bucket.candidates.Len() == 0 { return } - no = kbucket.candidates.Remove(kbucket.candidates.Back()).(*node) + no = bucket.candidates.Remove(bucket.candidates.Back()).(*node) inserted := false - for e := range kbucket.nodes.Iter() { + for e := range bucket.nodes.Iter() { if e.Value.(*node).lastActiveTime.After( no.lastActiveTime) && !inserted { - kbucket.nodes.InsertBefore(no, e) + bucket.nodes.InsertBefore(no, e) inserted = true } } if !inserted { - kbucket.nodes.PushBack(no) + bucket.nodes.PushBack(no) } } -// Clear resets the kbucket. -func (kbucket *kBucket) Clear() { - kbucket.Lock() - defer kbucket.Unlock() +// Clear resets the bucket. +func (bucket *kbucket) Clear() { + bucket.Lock() + defer bucket.Unlock() - kbucket.nodes.Clear() - kbucket.candidates.Clear() - kbucket.lastChanged = time.Now() + bucket.nodes.Clear() + bucket.candidates.Clear() + bucket.lastChanged = time.Now() } -// Fresh pings the expired nodes in the kbucket. -func (kbucket *kBucket) Fresh(dht *DHT) { - for e := range kbucket.nodes.Iter() { +// Fresh pings the expired nodes in the bucket. +func (bucket *kbucket) Fresh(dht *DHT) { + for e := range bucket.nodes.Iter() { no := e.Value.(*node) if time.Since(no.lastActiveTime) > dht.NodeExpriedAfter { dht.transactionManager.ping(no) @@ -251,14 +251,14 @@ func (kbucket *kBucket) Fresh(dht *DHT) { type routingTableNode struct { sync.RWMutex children []*routingTableNode - kbucket *kBucket + bucket *kbucket } // newRoutingTableNode returns a new routingTableNode pointer. func newRoutingTableNode(prefix *bitmap) *routingTableNode { return &routingTableNode{ children: make([]*routingTableNode, 2), - kbucket: newKBucket(prefix), + bucket: newKBucket(prefix), } } @@ -283,20 +283,20 @@ func (tableNode *routingTableNode) SetChild(index int, c *routingTableNode) { tableNode.children[index] = c } -// KBucket returns the kbucket routingTableNode holds. -func (tableNode *routingTableNode) KBucket() *kBucket { +// KBucket returns the bucket routingTableNode holds. +func (tableNode *routingTableNode) KBucket() *kbucket { tableNode.RLock() defer tableNode.RUnlock() - return tableNode.kbucket + return tableNode.bucket } -// SetKBucket sets the kbucket. -func (tableNode *routingTableNode) SetKBucket(kbucket *kBucket) { +// SetKBucket sets the bucket. +func (tableNode *routingTableNode) SetKBucket(bucket *kbucket) { tableNode.Lock() defer tableNode.Unlock() - tableNode.kbucket = kbucket + tableNode.bucket = bucket } // Split splits current routingTableNode and sets it's two children. @@ -313,7 +313,7 @@ func (tableNode *routingTableNode) Split(rt *routingTable) { } tableNode.Lock() - tableNode.children[1].kbucket.prefix.Set(prefixLen) + tableNode.children[1].bucket.prefix.Set(prefixLen) tableNode.Unlock() for e := range tableNode.KBucket().nodes.Iter() { @@ -340,7 +340,7 @@ func (tableNode *routingTableNode) Clear() { defer tableNode.Unlock() tableNode.children = make([]*routingTableNode, 2) - tableNode.kbucket.Clear() + tableNode.bucket.Clear() } // routingTable implements the routing table in DHT protocol. @@ -366,15 +366,15 @@ func newRoutingTable(k int, dht *DHT) *routingTable { dht: dht, } - rt.cachedKBuckets.Push(root.kbucket.prefix.String(), root.kbucket) + rt.cachedKBuckets.Push(root.bucket.prefix.String(), root.bucket) return rt } -// Touch updates the routing table. It will put the kbucket to the end of -// kbucket list. -func (rt *routingTable) Touch(kbucket *kBucket) { - kbucket.UpdateTimestamp() - rt.cachedKBuckets.Push(kbucket.prefix.String(), kbucket) +// Touch updates the routing table. It will put the bucket to the end of +// bucket list. +func (rt *routingTable) Touch(bucket *kbucket) { + bucket.UpdateTimestamp() + rt.cachedKBuckets.Push(bucket.prefix.String(), bucket) } // Insert adds a node to routing table. It returns whether the node is new @@ -402,12 +402,12 @@ func (rt *routingTable) Insert(nd *node) bool { return root.KBucket().Insert(nd, rt) } else if root.KBucket().prefix.Compare(nd.id, prefixLen-1) == 0 { - // If node has the same prefix with kbucket, split it. + // If node has the same prefix with bucket, split it. root.Split(rt) root = root.Child(nd.id.Bit(prefixLen - 1)) } else { - // Finally, store node as a candidate and fresh the kbucket. + // Finally, store node as a candidate and fresh the bucket. root.KBucket().candidates.PushBack(nd) if root.KBucket().candidates.Len() > rt.k { root.KBucket().candidates.Remove( @@ -451,10 +451,10 @@ func (rt *routingTable) GetNeighborCompactInfos(id *bitmap, size int) []string { return infos } -// GetNodeKBucktById returns node whose id is `id` and the kbucket it +// GetNodeKBucktById returns node whose id is `id` and the bucket it // belongs to. -func (rt *routingTable) GetNodeKBucktById(id *bitmap) ( - nd *node, kbucket *kBucket) { +func (rt *routingTable) GetNodeKBucktByID(id *bitmap) ( + nd *node, bucket *kbucket) { rt.RLock() defer rt.RUnlock() @@ -469,7 +469,7 @@ func (rt *routingTable) GetNodeKBucktById(id *bitmap) ( if !ok { return } - nd, kbucket = v.Value.(*node), root.KBucket() + nd, bucket = v.Value.(*node), root.KBucket() return } root = next @@ -491,9 +491,9 @@ func (rt *routingTable) GetNodeByAddress(address string) (no *node, ok bool) { // Remove deletes the node whose id is `id`. func (rt *routingTable) Remove(id *bitmap) { - if nd, kbucket := rt.GetNodeKBucktById(id); nd != nil { + if nd, bucket := rt.GetNodeKBucktByID(id); nd != nil { rt.Lock() - kbucket.Replace(nd, rt) + bucket.Replace(nd, rt) rt.Unlock() } } @@ -514,7 +514,7 @@ func (rt *routingTable) Clear() { rt.root.Clear() rt.cachedNodes.Clear() rt.cachedKBuckets.Clear() - rt.cachedKBuckets.Push(rt.root.kbucket.prefix.String(), rt.root.kbucket) + rt.cachedKBuckets.Push(rt.root.bucket.prefix.String(), rt.root.bucket) } // Fresh sends findNode to all nodes in the expired nodes. @@ -522,15 +522,15 @@ func (rt *routingTable) Fresh() { now := time.Now() for e := range rt.cachedKBuckets.Iter() { - kbucket := e.Value.(*kBucket) - if now.Sub(kbucket.LastChanged()) < rt.dht.KBucketExpiredAfter || - kbucket.nodes.Len() == 0 { + bucket := e.Value.(*kbucket) + if now.Sub(bucket.LastChanged()) < rt.dht.KBucketExpiredAfter || + bucket.nodes.Len() == 0 { continue } - for e := range kbucket.nodes.Iter() { + for e := range bucket.nodes.Iter() { no := e.Value.(*node) - rt.dht.transactionManager.findNode(no, kbucket.RandomChildID()) + rt.dht.transactionManager.findNode(no, bucket.RandomChildID()) } } @@ -547,7 +547,7 @@ func (rt *routingTable) Len() int { return rt.cachedNodes.Len() } -// Implemention of heap with heap.Interface. +// Implementation of heap with heap.Interface. type heapItem struct { distance *bitmap value interface{} diff --git a/sample/spider/spider.go b/sample/spider/spider.go index 78361a4..55c779f 100644 --- a/sample/spider/spider.go +++ b/sample/spider/spider.go @@ -7,15 +7,15 @@ import ( "github.com/shiyanhui/dht" ) -type File struct { +type file struct { Path []interface{} `json:"path"` Length int `json:"length"` } -type BitTorrent struct { +type bitTorrent struct { InfoHash string `json:"infohash"` Name string `json:"name"` - Files []File `json:"files,omitempty"` + Files []file `json:"files,omitempty"` Length int `json:"length,omitempty"` } @@ -33,20 +33,20 @@ func main() { continue } - bt := BitTorrent{ + bt := bitTorrent{ InfoHash: hex.EncodeToString(resp.InfoHash), Name: info["name"].(string), } if v, ok := info["files"]; ok { files := v.([]interface{}) - bt.Files = make([]File, len(files)) + bt.Files = make([]file, len(files)) for i, item := range files { - file := item.(map[string]interface{}) - bt.Files[i] = File{ - Path: file["path"].([]interface{}), - Length: file["length"].(int), + f := item.(map[string]interface{}) + bt.Files[i] = file{ + Path: f["path"].([]interface{}), + Length: f["length"].(int), } } } else if _, ok := info["length"]; ok {